001/*
002 *  Copyright 2023 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 */
016package org.ametys.plugins.forms.actions;
017
018import java.util.List;
019import java.util.Map;
020import java.util.Optional;
021
022import org.apache.avalon.framework.service.ServiceException;
023import org.apache.avalon.framework.service.ServiceManager;
024import org.apache.cocoon.acting.ServiceableAction;
025import org.apache.cocoon.environment.Request;
026
027import org.ametys.core.right.RightManager;
028import org.ametys.core.user.CurrentUserProvider;
029import org.ametys.plugins.forms.dao.FormDAO;
030import org.ametys.plugins.forms.dao.FormEntryDAO;
031import org.ametys.plugins.forms.dao.FormQuestionDAO;
032import org.ametys.plugins.forms.dao.FormQuestionDAO.FormEntryValues;
033import org.ametys.plugins.forms.helper.FormWorkflowHelper;
034import org.ametys.plugins.forms.question.FormQuestionType;
035import org.ametys.plugins.forms.question.sources.ChoiceSourceType;
036import org.ametys.plugins.forms.question.types.impl.ChoicesListQuestionType;
037import org.ametys.plugins.forms.question.types.impl.ComputedQuestionType;
038import org.ametys.plugins.forms.question.types.impl.FileQuestionType;
039import org.ametys.plugins.forms.repository.Form;
040import org.ametys.plugins.forms.repository.FormEntry;
041import org.ametys.plugins.forms.repository.FormQuestion;
042import org.ametys.plugins.repository.AmetysObjectResolver;
043import org.ametys.plugins.repository.data.holder.values.UntouchedValue;
044import org.ametys.runtime.i18n.I18nizableText;
045import org.ametys.runtime.model.View;
046import org.ametys.runtime.model.ViewItem;
047import org.ametys.web.FOAmetysObjectCreationHelper;
048
049import com.google.common.collect.ArrayListMultimap;
050import com.google.common.collect.Multimap;
051
052/**
053 * Abstract action to process form entry inputs
054 */
055public abstract class AbstractProcessFormAction extends ServiceableAction
056{
057    /** The ametys object resolver */
058    protected AmetysObjectResolver _resolver;
059    
060    /** The FO ametys object creation helper */
061    protected FOAmetysObjectCreationHelper _foAmetysObjectCreationHelper;
062
063    /** The form DAO */
064    protected FormDAO _formDAO;
065    
066    /** The form entry DAO */
067    protected FormEntryDAO _entryDAO;
068    
069    /** The form question DAO */
070    protected FormQuestionDAO _formQuestionDAO;
071    
072    /** The right manager */
073    protected RightManager _rightManager;
074    
075    /** The observation manager */
076    
077    /** The current user provider */
078    protected CurrentUserProvider _currentUserProvider;
079
080    /** The form workflow helper */
081    protected FormWorkflowHelper _formWorkflowHelper;
082
083    @Override
084    public void service(ServiceManager smanager) throws ServiceException
085    {
086        super.service(smanager);
087        _foAmetysObjectCreationHelper = (FOAmetysObjectCreationHelper) smanager.lookup(FOAmetysObjectCreationHelper.ROLE);
088        _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE);
089        _formDAO = (FormDAO) smanager.lookup(FormDAO.ROLE);
090        _entryDAO = (FormEntryDAO) smanager.lookup(FormEntryDAO.ROLE);
091        _formQuestionDAO = (FormQuestionDAO) smanager.lookup(FormQuestionDAO.ROLE);
092        _rightManager = (RightManager) smanager.lookup(RightManager.ROLE);        
093        _currentUserProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE);
094        _formWorkflowHelper = (FormWorkflowHelper) smanager.lookup(FormWorkflowHelper.ROLE);
095    }
096    
097    /**
098     * Remove empty value for choice list because empty value is an other value
099     * @param form the form
100     * @param formInputValues the form inputs to change
101     */
102    protected void _adaptFormValuesForChoiceList(Form form, Map<String, Object> formInputValues)
103    {
104        List<FormQuestion> choiceListQuestions = form.getQuestions()
105            .stream()
106            .filter(q -> q.getType() instanceof ChoicesListQuestionType)
107            .toList();
108        
109        for (FormQuestion question : choiceListQuestions)
110        {
111            ChoicesListQuestionType type = (ChoicesListQuestionType) question.getType();
112            ChoiceSourceType sourceType = type.getSourceType(question);
113            
114            String nameForForm = question.getNameForForm();
115            if (formInputValues.containsKey(nameForForm))
116            {
117                Object object = formInputValues.get(nameForForm);
118                Object newValue = sourceType.removeEmptyOrOtherValue(object);
119                if (newValue == null)
120                {
121                    formInputValues.remove(nameForForm);
122                }
123                else
124                {
125                    formInputValues.put(nameForForm, newValue);
126                }
127                
128            }
129        }
130    }
131    
132    /**
133     * Adapt file question values to handle untouched file input.
134     * @param request the request containing form parameters
135     * @param form the form containing questions
136     * @param values the form values to adapt
137     */
138    protected void _adaptFormValuesForFile(Request request, Form form, Map<String, Object> values)
139    {
140        List<FormQuestion> fileQuestions = form.getQuestions()
141                .stream()
142                .filter(q -> q.getType() instanceof FileQuestionType)
143                .toList();
144            
145        for (FormQuestion question : fileQuestions)
146        {
147            String nameForForm = question.getNameForForm();
148            String fileInfo = request.getParameter(nameForForm + "-info");
149            if ("untouched".equals(fileInfo))
150            {
151                values.put(nameForForm, new UntouchedValue());
152            }
153        }
154    }
155    
156    /**
157     * Get a view without elements hidden by a rule
158     * @param request the request
159     * @param form The current form
160     * @param entryView The entry view with possibly unwanted viewItems
161     * @param entryValues The entry values 
162     * @param currentStepId current step of the entry. Can be empty if the form has no workflow
163     * @return a view with filtered items
164     */
165    protected View _getRuleFilteredEntryView(Request request, Form form, View entryView, FormEntryValues entryValues, Optional<Long> currentStepId)
166    {
167        View filteredEntryView = new View();
168        for (FormQuestion target : _getRuleFilteredQuestions(request, form, entryValues, currentStepId))
169        {
170            ViewItem viewItem = entryView.getViewItem(target.getNameForForm());
171            filteredEntryView.addViewItem(viewItem);
172            _manageOtherOption(entryView, filteredEntryView, target);
173        }
174        return filteredEntryView;
175    }
176    
177    /**
178     * Get the list of active question depending of the form rules
179     * @param request the request
180     * @param form the form
181     * @param entryValues the entry values to compute rules
182     * @param currentStepId the current step id. Can be empty if the form has no workflow
183     * @return the list of active question depending of the form rules
184     */
185    protected abstract List<FormQuestion> _getRuleFilteredQuestions(Request request, Form form, FormEntryValues entryValues, Optional<Long> currentStepId);
186    
187    private void _manageOtherOption(View entryView, View filteredEntryView, FormQuestion target)
188    {
189        if (target.getType() instanceof ChoicesListQuestionType type && type.hasOtherOption(target))
190        {
191            ViewItem viewOtherItem = entryView.getViewItem(ChoicesListQuestionType.OTHER_PREFIX_DATA_NAME + target.getNameForForm());
192            filteredEntryView.addViewItem(viewOtherItem);
193        }
194    }
195    
196    /**
197     * Handle computed values
198     * @param questions the form questions
199     * @param entry the entry
200     * @param forEdition <code>true</code> to handle edition
201     */
202    protected void _handleComputedValues(List<FormQuestion> questions, FormEntry entry, boolean forEdition)
203    {
204        for (FormQuestion question : questions)
205        {
206            FormQuestionType questionType = question.getType();
207            if (questionType instanceof ComputedQuestionType type)
208            {
209                if (!forEdition || type.getComputingType(question).canEdit())
210                {
211                    Object computedValue = type.getComputingType(question).getComputedValue(question, entry);
212                    if (computedValue != null)
213                    {
214                        entry.setValue(question.getNameForForm(), computedValue);
215                    }
216                }
217            }
218        }
219    }
220    
221    /**
222     * Edit form entry values with form inputs and validate them. If no error is found, values are saved in the entry and the form is saved.
223     * @param request the request containing form parameters
224     * @param entry the form entry to edit
225     * @param currentStepId current step of the entry. Can be empty if the form has no workflow
226     * @param additionalParameters additional parameters to provide to question type validation
227     * @param isCreated <code>true</code> if the entry is being created, <code>false</code> if it is an edition.
228     * @return the map of errors
229     * @throws Exception if an error occurred
230     */
231    protected Multimap<String, I18nizableText> editFormEntryValues(Request request, FormEntry entry, Optional<Long> currentStepId, Map<String, Object> additionalParameters, boolean isCreated) throws Exception
232    {
233        Multimap<String, I18nizableText> formErrors = ArrayListMultimap.create();
234        
235        Form form = entry.getForm();
236        View entryView = View.of(entry.getModel());
237        Map<String, Object> valuesFromRequest = _foAmetysObjectCreationHelper.getFormValues(request, entryView, "", formErrors);
238        _adaptFormValuesForChoiceList(form, valuesFromRequest);
239        _adaptFormValuesForFile(request, form, valuesFromRequest);
240
241        // In edition, we don't have all form entry values in the request, so we need to provide the entry to get the values and computed the view (depending on rules, rights, etc.)
242        // In creation, all values are provided by the request
243        FormEntryValues entryValues = isCreated ? new FormEntryValues(valuesFromRequest, null) : new FormEntryValues(null, entry);
244        View filteredEntryView = _getRuleFilteredEntryView(request, form, entryView, entryValues, currentStepId);
245        formErrors.putAll(_foAmetysObjectCreationHelper.validateValues(valuesFromRequest, filteredEntryView));
246        for (FormQuestion question : form.getQuestions())
247        {
248            if (filteredEntryView.hasModelViewItem(question.getNameForForm()))
249            {
250                question.getType().validateEntryValues(question, valuesFromRequest, formErrors, currentStepId, additionalParameters);
251            }
252        }
253        
254        if (!formErrors.isEmpty())
255        {
256            return formErrors;
257        }
258        
259        _entryDAO.editFormEntry(entry, valuesFromRequest, filteredEntryView, isCreated);
260        
261        return formErrors;
262    }
263}