001/*
002 *  Copyright 2022 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.dao;
017
018import java.util.ArrayList;
019import java.util.HashMap;
020import java.util.LinkedHashMap;
021import java.util.List;
022import java.util.Map;
023import java.util.Set;
024
025import org.apache.avalon.framework.component.Component;
026import org.apache.avalon.framework.service.ServiceException;
027import org.apache.avalon.framework.service.ServiceManager;
028import org.apache.avalon.framework.service.Serviceable;
029import org.apache.cocoon.ProcessingException;
030import org.apache.commons.lang3.StringUtils;
031
032import org.ametys.core.observation.Event;
033import org.ametys.core.observation.ObservationManager;
034import org.ametys.core.right.RightManager;
035import org.ametys.core.right.RightManager.RightResult;
036import org.ametys.core.ui.Callable;
037import org.ametys.core.user.CurrentUserProvider;
038import org.ametys.core.user.UserIdentity;
039import org.ametys.plugins.forms.FormEvents;
040import org.ametys.plugins.forms.actions.GetFormEntriesAction;
041import org.ametys.plugins.forms.helper.FormElementDefinitionHelper;
042import org.ametys.plugins.forms.helper.LimitedEntriesHelper;
043import org.ametys.plugins.forms.question.FormQuestionType;
044import org.ametys.plugins.forms.question.types.ChoicesListQuestionType;
045import org.ametys.plugins.forms.question.types.FileQuestionType;
046import org.ametys.plugins.forms.question.types.MatrixQuestionType;
047import org.ametys.plugins.forms.question.types.MultipleAwareFormQuestionType;
048import org.ametys.plugins.forms.question.types.RichTextQuestionType;
049import org.ametys.plugins.forms.repository.Form;
050import org.ametys.plugins.forms.repository.FormEntry;
051import org.ametys.plugins.forms.repository.FormQuestion;
052import org.ametys.plugins.repository.AmetysObject;
053import org.ametys.plugins.repository.AmetysObjectResolver;
054import org.ametys.plugins.repository.AmetysRepositoryException;
055import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
056import org.ametys.plugins.repository.UnknownAmetysObjectException;
057import org.ametys.plugins.workflow.support.WorkflowProvider;
058import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow;
059import org.ametys.runtime.i18n.I18nizableText;
060import org.ametys.runtime.model.DefinitionContext;
061import org.ametys.runtime.model.ElementDefinition;
062import org.ametys.runtime.model.Model;
063import org.ametys.runtime.model.ModelItem;
064import org.ametys.runtime.model.View;
065import org.ametys.runtime.model.type.ModelItemTypeConstants;
066import org.ametys.runtime.plugin.component.AbstractLogEnabled;
067import org.ametys.web.parameters.ParametersManager;
068
069import com.opensymphony.workflow.spi.Step;
070
071/**
072 * Form entry DAO.
073 */
074public class FormEntryDAO extends AbstractLogEnabled implements Serviceable, Component
075{
076    /** The Avalon role name. */
077    public static final String ROLE = FormEntryDAO.class.getName();
078
079    /** Name for entries root jcr node */
080    public static final String ENTRIES_ROOT = "ametys-internal:form-entries";
081    
082    /** The right id to consult form entries */
083    public static final String HANDLE_FORMS_ENTRIES_RIGHT_ID = "Form_Entries_Rights_Data";
084    
085    /** The right id to delete form entries */
086    public static final String DELETE_FORMS_ENTRIES_RIGHT_ID = "Runtime_Rights_Forms_Entry_Delete";
087    
088    /** Ametys object resolver. */
089    protected AmetysObjectResolver _resolver;
090    /** The parameters manager */
091    protected ParametersManager _parametersManager;
092    /** Observer manager. */
093    protected ObservationManager _observationManager;
094    /** The current user provider. */
095    protected CurrentUserProvider _currentUserProvider;
096    /** The handling limited entries helper */
097    protected LimitedEntriesHelper _handleLimitedEntriesHelper;
098    /** The rights manager */
099    protected RightManager _rightManager;
100    /** The current user provider */
101    protected WorkflowProvider _workflowProvider;
102    
103    
104    @Override
105    public void service(ServiceManager serviceManager) throws ServiceException
106    {
107        _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
108        _parametersManager = (ParametersManager) serviceManager.lookup(ParametersManager.ROLE);
109        _observationManager = (ObservationManager) serviceManager.lookup(ObservationManager.ROLE);
110        _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE);
111        _handleLimitedEntriesHelper = (LimitedEntriesHelper) serviceManager.lookup(LimitedEntriesHelper.ROLE);
112        _rightManager = (RightManager) serviceManager.lookup(RightManager.ROLE);
113        _workflowProvider = (WorkflowProvider) serviceManager.lookup(WorkflowProvider.ROLE);
114    }
115    
116    /**
117     * Check if a user have handle data right on a form element as ametys object
118     * @param userIdentity the user
119     * @param formElement the form element
120     * @return true if the user handle data right for a form element
121     */
122    public boolean hasHandleDataRightOnForm(UserIdentity userIdentity, AmetysObject formElement)
123    {
124        return _rightManager.hasRight(userIdentity, HANDLE_FORMS_ENTRIES_RIGHT_ID, formElement) == RightResult.RIGHT_ALLOW;
125    }
126    
127    /**
128     * Check handle data right for a form element as ametys object
129     * @param formElement the form element as ametys object
130     */
131    public void checkHandleDataRight(AmetysObject formElement)
132    {
133        UserIdentity user = _currentUserProvider.getUser();
134        if (!hasHandleDataRightOnForm(user, formElement))
135        {
136            throw new IllegalAccessError("User '" + user + "' tried to handle form data without convenient right [" + HANDLE_FORMS_ENTRIES_RIGHT_ID + "]");
137        }
138    }
139    
140    /**
141     * Gets properties of a form entry
142     * @param id The id of the form entry
143     * @return The properties
144     */
145    @Callable
146    public Map<String, Object> getFormEntryProperties (String id)
147    {
148        try
149        {
150            FormEntry entry = _resolver.resolveById(id);
151            return getFormEntryProperties(entry);
152        }
153        catch (UnknownAmetysObjectException e)
154        {
155            getLogger().warn("Can't find entry with id: {}. It probably has just been deleted", id, e);
156            Map<String, Object> infos = new HashMap<>();
157            infos.put("id", id);
158            return infos;
159        }
160    }
161    
162    /**
163     * Gets properties of a form entry
164     * @param entry The form entry
165     * @return The properties
166     */
167    public Map<String, Object> getFormEntryProperties (FormEntry entry)
168    {
169        Map<String, Object> properties = new HashMap<>();
170        
171        properties.put("id", entry.getId());
172        properties.put("formId", entry.getForm().getId());
173        properties.put("rights", _getUserRights(entry));
174        
175        return properties;
176    }
177    
178    /**
179     * Get user rights for the given form entry
180     * @param entry the form entry
181     * @return the set of rights
182     */
183    protected Set<String> _getUserRights (FormEntry entry)
184    {
185        UserIdentity user = _currentUserProvider.getUser();
186        return _rightManager.getUserRights(user, entry);
187    }
188    
189    /**
190     * Creates a {@link FormEntry}.
191     * @param form The parent form
192     * @return return the form entry
193     */
194    public FormEntry createEntry(Form form)
195    {
196        ModifiableTraversableAmetysObject entriesRoot;
197        if (form.hasChild(ENTRIES_ROOT))
198        {
199            entriesRoot = form.getChild(ENTRIES_ROOT);
200        }
201        else
202        {
203            entriesRoot = form.createChild(ENTRIES_ROOT, "ametys:collection");
204        }
205        // Find unique name
206        String originalName = "entry";
207        String uniqueName = originalName + "-1";
208        int index = 2;
209        while (entriesRoot.hasChild(uniqueName))
210        {
211            uniqueName = originalName + "-" + (index++);
212        }
213        FormEntry entry =  (FormEntry) entriesRoot.createChild(uniqueName, "ametys:form-entry");
214        
215        Map<String, Object> eventParams = new HashMap<>();
216        eventParams.put("form", form);
217        _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _currentUserProvider.getUser(), eventParams));
218        
219        return entry;
220    }
221    
222    /**
223     * Get the search model configuration to search form entries
224     * @param formId the identifier of form
225     * @return The search model configuration
226     * @throws ProcessingException If an error occurred
227     */
228    @Callable
229    public Map<String, Object> getSearchModelConfiguration (String formId) throws ProcessingException
230    {
231        Map<String, Object> result = new HashMap<>();
232        
233        Form form = _resolver.resolveById(formId);
234        result.put("criteria", _getCriteria(form));
235        result.put("columns", _getColumns(form));
236
237        result.put("searchUrlPlugin", "forms");
238        result.put("searchUrl", "form/entries.json");
239        result.put("pageSize", 50);
240        return result;
241    }
242    
243    /**
244     * Get criteria to search form entries
245     * @param form the form
246     * @return the criteria as JSON
247     */
248    protected Map<String, Object> _getCriteria(Form form)
249    {
250        // Currently, return no criteria for search entries tool
251        return Map.of();
252    }
253    
254    /**
255     * Get the columns for search form entries
256     * @param form the form
257     * @return the columns as JSON
258     * @throws ProcessingException if an error occurred
259     */
260    protected List<Map<String, Object>> _getColumns(Form form) throws ProcessingException
261    {
262        List<Map<String, Object>> columns = new ArrayList<>();
263        columns.add(
264            Map.of("name", FormEntry.ATTRIBUTE_ID,
265                    "label", new I18nizableText("plugin.forms", "PLUGIN_FORMS_MODEL_ITEM_ID_LABEL"),
266                    "type", ModelItemTypeConstants.LONG_TYPE_ID,
267                    "id", FormEntry.ATTRIBUTE_ID,
268                    "width", 80
269             )
270        );
271        
272        columns.add(
273            Map.of("name", FormEntry.ATTRIBUTE_USER,
274                    "label", new I18nizableText("plugin.forms", "PLUGIN_FORMS_MODEL_ITEM_USER_LABEL"),
275                    "type", org.ametys.cms.data.type.ModelItemTypeConstants.USER_ELEMENT_TYPE_ID,
276                    "id", FormEntry.ATTRIBUTE_USER,
277                    "width", 150
278             )
279        );
280        
281        columns.add(
282            Map.of("name", FormEntry.ATTRIBUTE_SUBMIT_DATE,
283                    "label", new I18nizableText("plugin.forms", "PLUGIN_FORMS_MODEL_ITEM_SUBMISSION_DATE_LABEL"),
284                    "type", ModelItemTypeConstants.DATETIME_TYPE_ID,
285                    "id", FormEntry.ATTRIBUTE_SUBMIT_DATE,
286                    "width", 150
287             )
288        );
289        
290        Model model = getFormEntryModel(form);
291        Map<String, Object> json = View.of(model).toJSON(DefinitionContext.newInstance());
292        @SuppressWarnings("unchecked")
293        Map<String, Object> elements = (Map<String, Object>) json.get("elements");
294        for (String id : elements.keySet())
295        {
296            if (!id.equals(FormEntry.ATTRIBUTE_IP) 
297                 && !id.equals(FormEntry.ATTRIBUTE_ACTIVE) 
298                 && !id.equals(FormEntry.ATTRIBUTE_SUBMIT_DATE) 
299                 && !id.equals(FormEntry.ATTRIBUTE_ID) 
300                 && !id.equals(FormEntry.ATTRIBUTE_USER)
301                 && !id.startsWith(ChoicesListQuestionType.OTHER_PREFIX_DATA_NAME))
302            {
303                @SuppressWarnings("unchecked")
304                Map<String, Object> column = (Map<String, Object>) elements.get(id);
305                column.put("id", id);
306                
307                FormQuestion question = form.getQuestion(id);
308                if (question != null)
309                {
310                    FormQuestionType type = question.getType();
311                    String jsRenderer = type.getJSRenderer(question);
312                    if (StringUtils.isNotBlank(jsRenderer))
313                    {
314                        column.put("renderer", jsRenderer);
315                    }
316                    
317                    String jsConverter = type.getJSConverter(question);
318                    if (StringUtils.isNotBlank(jsConverter))
319                    {
320                        column.put("converter", jsConverter);
321                    }
322                    
323                    if (!_isSortable(question))
324                    {
325                        column.put("sortable", false);
326                    }
327                }
328                
329                columns.add(column);
330            }
331        }
332        
333        if (form.isQueueEnabled())
334        {
335            columns.add(
336                Map.of("name", FormEntry.SYSTEM_ATTRIBUTE_PREFIX + GetFormEntriesAction.QUEUE_STATUS,
337                        "label", new I18nizableText("plugin.forms", "PLUGINS_FORMS_QUEUE_STATUS_COLUMN_TITLE_LABEL"),
338                        "type", ModelItemTypeConstants.BOOLEAN_TYPE_ID,
339                        "id", FormEntry.SYSTEM_ATTRIBUTE_PREFIX + GetFormEntriesAction.QUEUE_STATUS
340                 )
341            );
342        }
343        
344        if (form.hasWorkflow())
345        {
346            columns.add(
347                Map.of("name", FormEntry.SYSTEM_ATTRIBUTE_PREFIX + GetFormEntriesAction.FORM_ENTRY_STATUS_ID,
348                       "label", new I18nizableText("plugin.forms", "PLUGINS_FORMS_WORKFLOW_TAB_STATUS_COLUMN_TITLE_LABEL"),
349                       "type", ModelItemTypeConstants.STRING_TYPE_ID,
350                       "id", FormEntry.SYSTEM_ATTRIBUTE_PREFIX + GetFormEntriesAction.FORM_ENTRY_STATUS_ID
351                )
352            );
353        }
354        
355        columns.add(
356            Map.of("name", FormEntry.SYSTEM_ATTRIBUTE_PREFIX + GetFormEntriesAction.FORM_ENTRY_ACTIVE,
357                    "label", new I18nizableText("plugin.forms", "PLUGINS_FORMS_ENTRY_ACTIVE_COLUMN_TITLE_LABEL"),
358                    "type", ModelItemTypeConstants.BOOLEAN_TYPE_ID,
359                    "id", FormEntry.SYSTEM_ATTRIBUTE_PREFIX + GetFormEntriesAction.FORM_ENTRY_ACTIVE,
360                    "hidden", true
361             )
362        );
363        
364        return columns;
365    }
366    
367    /**
368     * <code>true</code> if the column link to the question is sortable
369     * @param question the question
370     * @return <code>true</code> if the column link to the question is sortable
371     */
372    protected boolean _isSortable(FormQuestion question)
373    {
374        FormQuestionType type = question.getType();
375        if (type instanceof MultipleAwareFormQuestionType multipleType && multipleType.isMultiple(question))
376        {
377            return false;
378        }
379        
380        if (type instanceof MatrixQuestionType || type instanceof RichTextQuestionType || type instanceof FileQuestionType)
381        {
382            return false;
383        }
384        
385        return true;
386    }
387
388    /**
389     * Deletes a {@link FormEntry}.
390     * @param id The id of the form entry to delete
391     * @return The entry data
392     */
393    @Callable
394    public Map<String, String> deleteEntry (String id)
395    {
396        Map<String, String> result = new HashMap<>();
397        
398        FormEntry entry = _resolver.resolveById(id);
399        
400        if (_rightManager.currentUserHasRight(DELETE_FORMS_ENTRIES_RIGHT_ID, entry) != RightResult.RIGHT_ALLOW)
401        {
402            throw new IllegalAccessError("User '" + _currentUserProvider.getUser() + "' tried to delete entries without convenient right [" + DELETE_FORMS_ENTRIES_RIGHT_ID + "]");
403        }
404        
405        _handleLimitedEntriesHelper.deactivateEntry(id);
406        
407        Form form = entry.getForm();
408        entry.remove();
409        
410        form.saveChanges();
411        
412        Map<String, Object> eventParams = new HashMap<>();
413        eventParams.put("form", form);
414        _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _currentUserProvider.getUser(), eventParams));
415        
416        result.put("entryId", id);
417        result.put("formId", form.getId());
418        result.put("hasEntries", String.valueOf(form.hasEntries()));
419        
420        return result;
421    }
422    
423    /**
424     * Delete all entries of a form
425     * @param id The id of the form
426     * @return the deleted entries data
427     */
428    @Callable
429    public Map<String, Object> clearEntries(String id)
430    {
431        Map<String, Object> result = new HashMap<>();
432        List<String> entryIds = new ArrayList<>();
433        Form form = _resolver.resolveById(id);
434        if (_rightManager.currentUserHasRight(DELETE_FORMS_ENTRIES_RIGHT_ID, form) != RightResult.RIGHT_ALLOW)
435        {
436            throw new IllegalAccessError("User '" + _currentUserProvider.getUser() + "' tried to delete entries without convenient right [" + DELETE_FORMS_ENTRIES_RIGHT_ID + "]");
437        }
438        
439        for (FormEntry entry: form.getEntries())
440        {
441            entryIds.add(entry.getId());
442            entry.remove();
443        }
444        form.saveChanges();
445        Map<String, Object> eventParams = new HashMap<>();
446        eventParams.put("form", form);
447        _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _currentUserProvider.getUser(), eventParams));
448        
449        result.put("ids", entryIds);
450        result.put("formId", form.getId());
451        
452        return result;
453    }
454
455    /**
456     * Retrieves the current step id of the form entry
457     * @param entry The form entry
458     * @return the current step id
459     * @throws AmetysRepositoryException if an error occurs.
460     */
461    public Long getCurrentStepId(FormEntry entry) throws AmetysRepositoryException
462    {
463        AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(entry);
464        Step currentStep = (Step) workflow.getCurrentSteps(entry.getWorkflowId()).iterator().next();
465        return Long.valueOf(currentStep.getStepId());
466    }
467    
468    /**
469     * Get the form entry model
470     * @param form the form
471     * @return the form entry model
472     */
473    public Model getFormEntryModel(Form form)
474    {
475        Map<String, ModelItem> items = new LinkedHashMap<>();
476        for (FormQuestion question : form.getQuestions())
477        {
478            FormQuestionType type = question.getType();
479            if (!type.onlyForDisplay(question))
480            {
481                Model entryModel = question.getType().getEntryModel(question);
482                for (ModelItem modelItem : entryModel.getModelItems())
483                {
484                    items.put(modelItem.getName(), modelItem);
485                }
486                
487                if (type instanceof ChoicesListQuestionType cLType)
488                {
489                    ModelItem otherFieldModel = cLType.getOtherFieldModel(question);
490                    if (otherFieldModel != null)
491                    {
492                        items.put(ChoicesListQuestionType.OTHER_PREFIX_DATA_NAME + question.getNameForForm(), otherFieldModel);
493                    }
494                }
495            }
496        }
497        
498        ElementDefinition idModelItem = FormElementDefinitionHelper.getElementDefinition(FormEntry.ATTRIBUTE_ID, ModelItemTypeConstants.LONG_TYPE_ID, "PLUGIN_FORMS_MODEL_ITEM_ID_LABEL", null, null);
499        items.put(idModelItem.getName(), idModelItem);
500        
501        ElementDefinition userModelItem = FormElementDefinitionHelper.getElementDefinition(FormEntry.ATTRIBUTE_USER, org.ametys.cms.data.type.ModelItemTypeConstants.USER_ELEMENT_TYPE_ID, "PLUGIN_FORMS_MODEL_ITEM_USER_LABEL", null, null);
502        items.put(userModelItem.getName(), userModelItem);
503        
504        ElementDefinition activeModelItem = FormElementDefinitionHelper.getElementDefinition(FormEntry.ATTRIBUTE_ACTIVE, ModelItemTypeConstants.BOOLEAN_TYPE_ID, "PLUGIN_FORMS_MODEL_ITEM_ACTIVE_LABEL", null, null);
505        items.put(activeModelItem.getName(), activeModelItem);
506        
507        ElementDefinition submitDateModelItem = FormElementDefinitionHelper.getElementDefinition(FormEntry.ATTRIBUTE_SUBMIT_DATE, ModelItemTypeConstants.DATETIME_TYPE_ID, "PLUGIN_FORMS_MODEL_ITEM_SUBMISSION_DATE_LABEL", null, null);
508        items.put(submitDateModelItem.getName(), submitDateModelItem);
509        
510        ElementDefinition ipAddressModelItem = FormElementDefinitionHelper.getElementDefinition(FormEntry.ATTRIBUTE_IP, ModelItemTypeConstants.STRING_TYPE_ID, "PLUGIN_FORMS_MODEL_ITEM_IP_LABEL", null, null);
511        items.put(ipAddressModelItem.getName(), ipAddressModelItem);
512        
513        return Model.of(
514            "form.entry.model.id", 
515            "form.entry.model.family.id", 
516            items.values().toArray(new ModelItem[items.size()])
517        );
518    }
519    
520}