001/*
002 *  Copyright 2021 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016
017package org.ametys.plugins.forms.dao;
018
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.Collection;
022import java.util.HashMap;
023import java.util.List;
024import java.util.Map;
025import java.util.Optional;
026import java.util.Set;
027
028import org.apache.avalon.framework.component.Component;
029import org.apache.avalon.framework.context.Context;
030import org.apache.avalon.framework.context.ContextException;
031import org.apache.avalon.framework.context.Contextualizable;
032import org.apache.avalon.framework.service.ServiceException;
033import org.apache.avalon.framework.service.ServiceManager;
034import org.apache.avalon.framework.service.Serviceable;
035import org.apache.cocoon.Constants;
036import org.apache.cocoon.ProcessingException;
037import org.apache.commons.collections.ListUtils;
038import org.apache.commons.lang.StringUtils;
039import org.apache.commons.lang3.ArrayUtils;
040
041import org.ametys.core.observation.Event;
042import org.ametys.core.observation.ObservationManager;
043import org.ametys.core.right.RightManager;
044import org.ametys.core.ui.Callable;
045import org.ametys.core.upload.UploadManager;
046import org.ametys.core.user.CurrentUserProvider;
047import org.ametys.core.user.UserIdentity;
048import org.ametys.core.util.I18nUtils;
049import org.ametys.core.util.JSONUtils;
050import org.ametys.plugins.forms.FormEvents;
051import org.ametys.plugins.forms.question.FormQuestionType;
052import org.ametys.plugins.forms.question.FormQuestionTypeExtensionPoint;
053import org.ametys.plugins.forms.question.sources.AbstractSourceType;
054import org.ametys.plugins.forms.question.sources.ChoiceOption;
055import org.ametys.plugins.forms.question.sources.ChoiceSourceType;
056import org.ametys.plugins.forms.question.sources.ChoiceSourceTypeExtensionPoint;
057import org.ametys.plugins.forms.question.types.ChoicesListQuestionType;
058import org.ametys.plugins.forms.repository.Form;
059import org.ametys.plugins.forms.repository.FormEntry;
060import org.ametys.plugins.forms.repository.FormPage;
061import org.ametys.plugins.forms.repository.FormPageRule;
062import org.ametys.plugins.forms.repository.FormPageRule.PageRuleType;
063import org.ametys.plugins.forms.repository.FormQuestion;
064import org.ametys.plugins.forms.repository.type.Rule;
065import org.ametys.plugins.forms.repository.type.Rule.QuestionRuleType;
066import org.ametys.plugins.repository.AmetysObjectResolver;
067import org.ametys.plugins.repository.UnknownAmetysObjectException;
068import org.ametys.plugins.repository.jcr.NameHelper;
069import org.ametys.runtime.i18n.I18nizableText;
070import org.ametys.runtime.model.DefinitionContext;
071import org.ametys.runtime.model.ElementDefinition;
072import org.ametys.runtime.model.Model;
073import org.ametys.runtime.model.ModelItem;
074import org.ametys.runtime.model.View;
075import org.ametys.runtime.plugin.component.AbstractLogEnabled;
076import org.ametys.web.parameters.ParametersManager;
077
078/** DAO for manipulating form questions */
079public class FormQuestionDAO extends AbstractLogEnabled implements Serviceable, Component, Contextualizable
080{
081    /** The Avalon role */
082    public static final String ROLE = FormQuestionDAO.class.getName();
083
084    /** Name for rules root jcr node */
085    public static final String RULES_ROOT = "ametys-internal:form-page-rules";
086    
087    /** Ametys object resolver. */
088    protected AmetysObjectResolver _resolver;
089    /** Observer manager. */
090    protected ObservationManager _observationManager;
091    /** The current user provider. */
092    protected CurrentUserProvider _currentUserProvider;
093    /** Manager for retrieving uploaded files */
094    protected UploadManager _uploadManager;
095    /** JSON helper */
096    protected JSONUtils _jsonUtils;
097    /** I18n Utils */
098    protected I18nUtils _i18nUtils;
099    /** The form question type extension point */
100    protected FormQuestionTypeExtensionPoint _formQuestionTypeExtensionPoint;
101    /** The parameters manager */
102    protected ParametersManager _parametersManager;
103    /** The Avalon context */
104    protected Context _context;
105    /** The cocoon context */
106    protected org.apache.cocoon.environment.Context _cocoonContext;
107    /** The choice source type extension point */
108    protected ChoiceSourceTypeExtensionPoint _choiceSourceTypeExtensionPoint;
109    /**The form DAO */
110    protected FormDAO _formDAO;
111    /** The right manager */
112    protected RightManager _rightManager;
113    
114    @Override
115    public void service(ServiceManager serviceManager) throws ServiceException
116    {
117        _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
118        _observationManager = (ObservationManager) serviceManager.lookup(ObservationManager.ROLE);
119        _parametersManager = (ParametersManager) serviceManager.lookup(ParametersManager.ROLE);
120        _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE);
121        _uploadManager = (UploadManager) serviceManager.lookup(UploadManager.ROLE);
122        _jsonUtils = (JSONUtils) serviceManager.lookup(JSONUtils.ROLE);
123        _i18nUtils = (I18nUtils) serviceManager.lookup(I18nUtils.ROLE);
124        _formQuestionTypeExtensionPoint = (FormQuestionTypeExtensionPoint) serviceManager.lookup(FormQuestionTypeExtensionPoint.ROLE);
125        _parametersManager = (ParametersManager) serviceManager.lookup(ParametersManager.ROLE);
126        _formDAO = (FormDAO) serviceManager.lookup(FormDAO.ROLE);
127        _rightManager = (RightManager) serviceManager.lookup(RightManager.ROLE);
128    }
129    
130    @Override
131    public void contextualize(Context context) throws ContextException
132    {
133        _context = context;
134        _cocoonContext = (org.apache.cocoon.environment.Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
135    }
136    
137    /**
138     * Provides the current user.
139     * @return the user which cannot be <code>null</code>.
140     */
141    protected UserIdentity _getCurrentUser()
142    {      
143        return _currentUserProvider.getUser();
144    }
145    
146    /**
147     * Gets properties of a form question
148     * @param id The id of the form question
149     * @return The properties
150     */
151    @Callable
152    public Map<String, Object> getQuestionProperties (String id)
153    {
154        try
155        {
156            FormQuestion question = _resolver.resolveById(id);
157            return getQuestionProperties(question, true);
158        }
159        catch (UnknownAmetysObjectException e)
160        {
161            getLogger().warn("Can't find question with id: {}. It probably has just been deleted", id, e);
162            Map<String, Object> infos = new HashMap<>();
163            infos.put("id", id);
164            return infos;
165        }
166    }
167    
168    /**
169     * Gets properties of a form question
170     * @param question The form question
171     * @param withRight <code>true</code> to have the rights in the properties
172     * @return The properties
173     */
174    public Map<String, Object> getQuestionProperties (FormQuestion question, boolean withRight)
175    {
176        Map<String, Object> properties = new HashMap<>();
177        
178        boolean hasTerminalRule = _hasTerminalRule(question);
179        List<String> questionTitlesWithRule = _getQuestionTitlesWithRule(question);
180        List<String> pageTitlesWithRule = _getPageTitlesWithRule(question);
181
182        properties.put("type", "question"); 
183        properties.put("hasTerminalRule", hasTerminalRule);
184        properties.put("pageTitlesWithRule", pageTitlesWithRule);
185        properties.put("questionTitlesWithRule", questionTitlesWithRule);
186        properties.put("isReadRestricted", question.isReadRestricted());
187        properties.put("isModifiable", question.isModifiable());
188        properties.put("hasChildren", false);
189
190        /** Use in the bus message */
191        properties.put("id", question.getId());
192        properties.put("title", question.getTitle());
193        properties.put("questionType", question.getType().getId());
194        properties.put("pageId", question.getFormPage().getId());
195        properties.put("formId", question.getForm().getId());
196        properties.put("iconGlyph", question.getType().getIconGlyph());
197        properties.put("typeLabel", question.getType().getLabel());
198        properties.put("hasEntries", !question.getForm().getEntries().isEmpty());
199        properties.put("hasRule", hasTerminalRule || !pageTitlesWithRule.isEmpty() || !questionTitlesWithRule.isEmpty());
200        properties.put("isConfigured", question.getType().isQuestionConfigured(question));
201        
202        if (withRight)
203        {
204            properties.put("rights", _getUserRights(question));
205        }
206        else
207        {
208            properties.put("canWrite", _formDAO.hasWriteRightOnForm(_currentUserProvider.getUser(), question));
209        }
210        
211        return properties;
212    }
213    
214    /**
215     * Get options from the choice list question
216     * @param questionId the choice list question id
217     * @return the map of option
218     */
219    @Callable
220    public Map<String, I18nizableText> getChoiceListQuestionOptions (String questionId)
221    {
222        FormQuestion question = _resolver.resolveById(questionId);
223        if (question.getType() instanceof ChoicesListQuestionType type)
224        {
225            return type.getOptions(question);
226        }
227        
228        return new HashMap<>();
229    }
230
231    /**
232     * Get user rights for the given form question
233     * @param question the form question
234     * @return the set of rights
235     */
236    protected Set<String> _getUserRights (FormQuestion question)
237    {
238        UserIdentity user = _currentUserProvider.getUser();
239        return _rightManager.getUserRights(user, question);
240    }
241    
242    private boolean _hasTerminalRule(FormQuestion question)
243    {
244        return question.getPageRules()
245                .stream()
246                .map(FormPageRule::getType)
247                .filter(t -> t == PageRuleType.FINISH)
248                .findAny()
249                .isPresent();
250    }
251
252    /**
253     * Get the question titles having rule concerning the given question
254     * @param question the question
255     * @return the list of question titles
256     */
257    protected List<String> _getQuestionTitlesWithRule(FormQuestion question)
258    {
259        return question.getForm()
260            .getQuestionsRule(question.getId())
261            .keySet()
262            .stream()
263            .map(FormQuestion::getTitle)
264            .toList();
265    }
266    
267    /**
268     * Get the page titles having rule concerning the given question
269     * @param question the question
270     * @return the list of page titles
271     */
272    protected List<String> _getPageTitlesWithRule(FormQuestion question)
273    {
274        return question.getPageRules()
275            .stream()
276            .filter(r -> r.getType() != PageRuleType.FINISH)
277            .map(FormPageRule::getPageId)
278            .distinct()
279            .map(this::_getFormPage)
280            .map(FormPage::getTitle)
281            .toList();
282    }
283    
284    private FormPage _getFormPage(String pageId)
285    {
286        return _resolver.resolveById(pageId);
287    }
288    
289    /**
290     * Get view for question type
291     * @param typeID id of the question type
292     * @param formId id of the form
293     * @return the view parsed in json for configurableFormPanel 
294     * @throws ProcessingException  error while parsing view to json
295     */
296    @Callable
297    public Map<String, Object> getQuestionParametersDefinitions(String typeID, String formId) throws ProcessingException
298    {
299        Map<String, Object> response = new HashMap<>();
300        Form form = _resolver.resolveById(formId);
301        FormQuestionType questionType = _formQuestionTypeExtensionPoint.getExtension(typeID);
302        View view = questionType.getView(form);
303        response.put("parameters", view.toJSON(DefinitionContext.newInstance().withEdition(true)));
304        response.put("questionNames", form.getQuestionsNames());
305        return response;
306    }
307    
308    /**
309     * Get questions parameters values
310     * @param questionID id of current question
311     * @return map of question parameters value 
312     */
313    @Callable
314    public Map<String, Object> getQuestionParametersValues(String questionID)
315    {
316        Map<String, Object> results = new HashMap<>();
317        
318        FormQuestion question = _resolver.resolveById(questionID);
319        FormQuestionType type = question.getType();
320        Collection< ? extends ModelItem> questionModelItems = type.getModel().getModelItems();
321        Map<String, Object> parametersValues = _parametersManager.getParametersValues(questionModelItems, question, StringUtils.EMPTY);
322        results.put("values", parametersValues);
323        
324        // Repeater values aren't handled by getParametersValues()
325        @SuppressWarnings("unchecked")
326        List<Map<String, Object>> repeaters = _parametersManager.getRepeatersValues((Collection<ModelItem>) questionModelItems, question, StringUtils.EMPTY);
327        results.put("repeaters", repeaters);
328        results.put("fieldToDisable", _getFieldNameToDisable(question));
329        
330        return results;
331    }
332    
333    private List<String> _getFieldNameToDisable(FormQuestion question)
334    {
335        Form form = question.getForm();
336        if (form.getEntries().isEmpty())
337        {
338            return List.of();
339        }
340        
341        return question.getType().getFieldToDisableIfFormPublished(question);
342    }
343    
344    /**
345     * Creates a {@link FormQuestion}.
346     * @param pageId id of current page
347     * @param typeId id of FormQuestionType
348     * @return The id of the created form question, the id of the page and the id of the form
349     */
350    @Callable
351    public Map<String, Object> createQuestion(String pageId, String typeId)
352    {
353        Map<String, Object> result = new HashMap<>();
354        FormPage page = _resolver.resolveById(pageId);
355
356        _formDAO.checkHandleFormRight(page);
357        
358        FormQuestionType type = _formQuestionTypeExtensionPoint.getExtension(typeId);
359        Form form = page.getForm();
360        
361        String defaultTitle = _i18nUtils.translate(type.getDefaultTitle());
362        String nameForForm = form.findUniqueQuestionName(NameHelper.filterName(defaultTitle));
363        
364        String id = nameForForm;
365        int i = 1;
366        while (page.hasChild(id))
367        {
368            id = nameForForm + "-" + i;
369            i++;
370        }
371        FormQuestion question = page.createChild(id, "ametys:form-question");
372        question.setNameForForm(nameForForm);
373        question.setTypeId(typeId);
374        
375        Model model = question.getType().getModel();
376        for (ModelItem modelItem : model.getModelItems())
377        {
378            if (modelItem instanceof ElementDefinition)
379            {
380                Object defaultValue = ((ElementDefinition) modelItem).getDefaultValue();
381                if (defaultValue != null)
382                {
383                    question.setValue(modelItem.getPath(), defaultValue);
384                }
385            }
386        }
387        
388        question.setTitle(form.findUniqueQuestionTitle(defaultTitle));
389        
390        page.saveChanges();
391        
392        Map<String, Object> eventParams = new HashMap<>();
393        eventParams.put("form", page.getForm());
394        _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _getCurrentUser(), eventParams));
395        
396        result.put("id", question.getId());
397        result.put("pageId", page.getId());
398        result.put("formId", question.getForm().getId());
399        result.put("type", typeId);
400        return result;
401    }
402    
403    /**
404     * Rename a {@link FormQuestion}
405     * @param id The id of the question 
406     * @param newName The new name of the question
407     * @return A result map
408     */
409    @Callable
410    public Map<String, String> renameQuestion (String id, String newName)
411    {
412        Map<String, String> results = new HashMap<>();
413        
414        FormQuestion question = _resolver.resolveById(id);
415        _formDAO.checkHandleFormRight(question);
416        
417        question.setTitle(newName);
418        question.saveChanges();
419        
420        Map<String, Object> eventParams = new HashMap<>();
421        eventParams.put("form", question.getForm());
422        _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _getCurrentUser(), eventParams));
423        
424        results.put("id", id);
425        results.put("newName", newName);
426        results.put("formId", question.getForm().getId());
427        
428        return results;
429    }
430    
431    /**
432     * Edits a {@link FormQuestion}.
433     * @param questionId id of current question
434     * @param values The question's values
435     * @return The id of the edited form question, the id of the page and the id of the form
436     */
437    @Callable
438    public Map<String, Object> editQuestion (String questionId, Map<String, Object> values)
439    {
440        Map<String, Object> result = new HashMap<>();
441        Map<String, I18nizableText> errors = new HashMap<>();
442        
443        FormQuestion question = _resolver.resolveById(questionId);
444        _formDAO.checkHandleFormRight(question);
445        
446        Form parentForm = question.getForm();
447        String questionName = StringUtils.defaultString((String) values.get("name-for-form"));
448        
449        // if question can not be answered by user, id can't be changed and is unique by default
450        if (!question.getType().canBeAnsweredByUser(question) || questionName.equals(question.getNameForForm()) || parentForm.isQuestionNameUnique(questionName))
451        {
452            FormQuestionType type = question.getType();
453            type.validateQuestionValues(values, errors);
454
455            if (!errors.isEmpty())
456            {
457                result.put("errors", errors);
458                return result;
459            }
460
461            _parametersManager.setParameterValues(question.getDataHolder(), type.getModel().getModelItems(), values);
462            type.doAdditionalOperations(question, values);
463            
464            question.saveChanges();
465            
466            Map<String, Object> eventParams = new HashMap<>();
467            eventParams.put("form", parentForm);
468            _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _getCurrentUser(), eventParams));
469            
470            result.put("id", question.getId());
471            result.put("pageId", question.getParent().getId());
472            result.put("formId", parentForm.getId());
473            result.put("type", question.getType().toString());
474        }
475        else
476        {
477            errors.put("duplicate_name", new I18nizableText("plugin.forms", "PLUGINS_FORMS_QUESTIONS_SET_ID_ERROR"));
478            result.put("errors", errors);
479            getLogger().error("An error occurred creating the question. The identifier value '" + questionName + "' is already used.");
480        }
481        
482        return result;
483    }
484    
485    /**
486     * Deletes a {@link FormQuestion}.
487     * @param id The id of the form question to delete
488     * @return The id of the form question, the id of the page and the id of the form
489     */
490    @Callable
491    public Map<String, String> deleteQuestion (String id)
492    {
493        FormQuestion question = _resolver.resolveById(id);
494        _formDAO.checkHandleFormRight(question);
495        
496        question.getForm().deleteQuestionsRule(question.getId());
497        
498        FormPage page = question.getParent();
499        question.remove();
500        
501        page.saveChanges();
502        
503        Map<String, Object> eventParams = new HashMap<>();
504        eventParams.put("form", page.getForm());
505        _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _getCurrentUser(), eventParams));
506        
507        return Map.of("id", id);
508    }
509
510    /**
511     * Copies and pastes a form question.
512     * @param pageId The id of the page, target of the copy
513     * @param questionId The id of the question to copy
514     * @return The id of the created question, the id of the page and the id of the form
515     */
516    @Callable
517    public Map<String, String> copyQuestion(String pageId, String questionId)
518    {
519        Map<String, String> result = new HashMap<>();
520        
521        FormQuestion originalQuestion = _resolver.resolveById(questionId);
522        _formDAO.checkHandleFormRight(originalQuestion);
523        
524        FormPage parentPage = _resolver.resolveById(pageId);
525        
526        Form parentForm = parentPage.getForm();
527        
528        String questionName = parentForm.findUniqueQuestionName(originalQuestion.getNameForForm());
529        FormQuestion questionCopy = parentPage.createChild(questionName, "ametys:form-question");
530        originalQuestion.copyTo(questionCopy);
531        
532        String copyTitle = _i18nUtils.translate(new I18nizableText("plugin.forms", "PLUGIN_FORMS_TREE_COPY_NAME_PREFIX")) + originalQuestion.getTitle();
533        questionCopy.setTitle(parentForm.findUniqueQuestionTitle(copyTitle));
534        questionCopy.setTypeId(originalQuestion.getType().getId());
535        questionCopy.setNameForForm(questionName);
536
537        parentPage.saveChanges();
538        
539        Map<String, Object> eventParams = new HashMap<>();
540        eventParams.put("form", parentForm);
541        _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _getCurrentUser(), eventParams));
542        
543        result.put("id", questionCopy.getId());
544        result.put("pageId", parentPage.getId());
545        result.put("formId", parentForm.getId());
546        result.put("type", questionCopy.getType().getId());
547        
548        return result;
549    }
550    
551    /**
552     * Gets the page rules for a form question.
553     * @param id The id of the form question.
554     * @param number The question number
555     * @return The rules
556     * @throws Exception error while getting choice options
557     */
558    @Callable
559    public Map<String, Object> getRules (String id, int number) throws Exception
560    {
561        Map<String, Object> result = new HashMap<>();
562        
563        FormQuestion question = _resolver.resolveById(id);
564        _formDAO.checkHandleFormRight(question);
565        
566        FormQuestionType type = question.getType();
567        if (type instanceof ChoicesListQuestionType cLType)
568        {
569            ChoiceSourceType sourceType = cLType.getSourceType(question);
570          
571            result.put("id", question.getId());
572            result.put("number", String.valueOf(number));
573            result.put("title", question.getTitle());
574            
575            List<Object> rules = new ArrayList<>();
576            for (FormPageRule rule : question.getPageRules())
577            {
578                String option = rule.getOption();
579                Map<String, Object> enumParam = new HashMap<>();
580                enumParam.put(AbstractSourceType.QUESTION_PARAM_KEY, question);
581                I18nizableText label = sourceType.getEntry(new ChoiceOption(option), enumParam);
582                
583                Map<String, Object> resultRule = new HashMap<>();
584                resultRule.put("option", option);
585                resultRule.put("optionLabel", label);
586                resultRule.put("type", rule.getType());
587                String pageId = rule.getPageId();
588                if (pageId != null)
589                {
590                    try
591                    {
592                        FormPage page = _resolver.resolveById(pageId);
593                        resultRule.put("page", pageId);
594                        resultRule.put("pageName", page.getTitle());
595                    }
596                    catch (UnknownAmetysObjectException e)
597                    {
598                        // Page does not exist anymore
599                    }
600                }
601                        
602                rules.add(resultRule);
603            }
604            
605            result.put("rules", rules);
606        }
607        
608        return result;
609    }
610
611    /**
612     * Adds a new rule to a question.
613     * @param id The question id
614     * @param option The option
615     * @param rule The rule type
616     * @param page The page to jump or skip
617     * @return An empty map, or an error
618     */
619    @Callable
620    public Map<String, Object> addPageRule (String id, String option, String rule, String page)
621    {
622        Map<String, Object> result = new HashMap<>();
623        
624        FormQuestion question = _resolver.resolveById(id);
625        _formDAO.checkHandleFormRight(question);
626        
627        // Check if exists
628        if (question.hasPageRule(option))
629        {
630            result.put("error", "already-exists");
631            return result;
632        }
633        
634        question.addPageRules(option, PageRuleType.valueOf(rule), page);
635        question.saveChanges();
636        
637        Map<String, Object> eventParams = new HashMap<>();
638        eventParams.put("form", question.getForm());
639        _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _getCurrentUser(), eventParams));
640        
641        result.put("id", question.getId());
642        result.put("pageId", question.getFormPage().getId());
643        result.put("formId", question.getForm().getId());
644        result.put("type", question.getType().getId());
645        return result;
646    }
647    
648    /**
649     * Deletes a rule to a question.
650     * @param id The question id
651     * @param option The option to delete
652     * @return An empty map
653     */
654    @Callable
655    public Map<String, Object> deletePageRule (String id, String option)
656    {
657        FormQuestion question = _resolver.resolveById(id);
658        _formDAO.checkHandleFormRight(question);
659        
660        question.deletePageRule(option);
661        question.saveChanges();
662        
663        Map<String, Object> eventParams = new HashMap<>();
664        eventParams.put("form", question.getForm());
665        _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _getCurrentUser(), eventParams));
666        
667        return new HashMap<>();
668    }
669    
670    /**
671     * Record for entry values coming from input or from the entry
672     * @param inputValues the inputValues. Can be null if the entry is not null
673     * @param entry the form entry. Can be null if the input values is not null
674     */
675    public record FormEntryValues(Map<String, Object> inputValues, FormEntry entry) 
676    {
677        Object getValue(String attributeName)
678        {
679            if (inputValues != null)
680            {
681                return inputValues.get(attributeName);
682            }
683            else
684            {
685                return entry.getValue(attributeName);
686            }
687        }
688    }
689    
690    /**
691     * Get the list of active question depending of the form rules
692     * @param form the form
693     * @param entryValues the entry values to compute rules
694     * @param currentStepId the current step id. Can be empty if the form has no workflow
695     * @param onlyWritableQuestion <code>true</code> to have only writable question
696     * @param onlyReadableQuestion <code>true</code> to have only readable question
697     * @return the list of active question depending of the form rules
698     */
699    public List<FormQuestion> getRuleFilteredQuestions(Form form, FormEntryValues entryValues, Optional<Long> currentStepId, boolean onlyWritableQuestion, boolean onlyReadableQuestion)
700    {
701        List<FormQuestion> filteredQuestions = new ArrayList<>();
702        for (FormQuestion activeQuestion : _getActiveQuestions(form, entryValues, currentStepId, onlyWritableQuestion, onlyReadableQuestion))
703        {
704            if (!activeQuestion.getType().onlyForDisplay(activeQuestion))
705            {
706                Optional<Rule> firstQuestionRule = activeQuestion.getFirstQuestionRule();
707                if (firstQuestionRule.isPresent())
708                {
709                    Rule rule = firstQuestionRule.get();
710                    FormQuestion sourceQuestion = _resolver.resolveById(rule.getSourceId());
711                    List<String> ruleValues = _getRuleValues(entryValues, sourceQuestion.getNameForForm());
712                    boolean equalsRuleOption = ruleValues.contains(rule.getOption());
713                    QuestionRuleType ruleAction = rule.getAction();
714                    
715                    if (!equalsRuleOption && ruleAction.equals(QuestionRuleType.HIDE)
716                            || equalsRuleOption && ruleAction.equals(QuestionRuleType.SHOW))
717                    {
718                        filteredQuestions.add(activeQuestion);
719                    }
720                }
721                else
722                {
723                    filteredQuestions.add(activeQuestion);
724                }
725            }
726        }
727        
728        return filteredQuestions;
729    }
730    
731    /**
732     * Get a list of the form questions not being hidden by a rule
733     * @param form the current form
734     * @param entryValues the entry values
735     * @param currentStepId current step of the entry. Can be empty if the form has no workflow
736     * @param onlyWritableQuestion <code>true</code> to have only writable question
737     * @param onlyReadableQuestion <code>true</code> to have only readable question
738     * @return a list of visible questions
739     */
740    protected List<FormQuestion> _getActiveQuestions(Form form, FormEntryValues entryValues, Optional<Long> currentStepId, boolean onlyWritableQuestion, boolean onlyReadableQuestion)
741    {
742        String nextActivePage = null;
743        List<FormQuestion> activeQuestions = new ArrayList<>();
744        for (FormPage page : form.getPages())
745        {
746            if (nextActivePage == null || page.getId().equals(nextActivePage))
747            {
748                nextActivePage = null;
749                for (FormQuestion question : page.getQuestions())
750                {
751                    if (currentStepId.isEmpty() // no current step id, ignore rights access
752                        || (!onlyReadableQuestion || question.canRead(currentStepId.get())) 
753                            && 
754                           (!onlyWritableQuestion || question.canWrite(currentStepId.get())))
755                    {
756                        activeQuestions.add(question);
757                    }
758                    
759                    if (question.getType() instanceof ChoicesListQuestionType type && !type.getSourceType(question).remoteData())
760                    {
761                        List<String> ruleValues = _getRuleValues(entryValues, question.getNameForForm());
762                        for (FormPageRule rule : question.getPageRules())
763                        {
764                            if (ruleValues.contains(rule.getOption()))
765                            {
766                                nextActivePage = _getNextActivePage(rule);
767                            }
768                        }
769                    }
770                }
771            }
772            
773            FormPageRule rule = page.getRule();
774            if (rule != null && nextActivePage == null)
775            {
776                nextActivePage = _getNextActivePage(rule);
777            }
778        }
779        return activeQuestions;
780    }
781
782    private String _getNextActivePage(FormPageRule rule)
783    {
784        return rule.getType() == PageRuleType.FINISH
785                ? "finish"
786                : rule.getPageId();
787    }
788
789    private List<String> _getRuleValues(FormEntryValues entryValues, String nameForForm)
790    {
791        Object ruleValue =  entryValues.getValue(nameForForm);
792        if (ruleValue == null)
793        {
794            return ListUtils.EMPTY_LIST;
795        }
796        
797        if (ruleValue.getClass().isArray())
798        {
799            String[] stringArray = ArrayUtils.toStringArray((Object[]) ruleValue);
800            return Arrays.asList(stringArray);
801        }
802        else
803        {
804            return List.of(ruleValue.toString());
805        }
806    }
807}