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}