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.Collection;
020import java.util.HashMap;
021import java.util.List;
022import java.util.Map;
023
024import org.apache.avalon.framework.component.Component;
025import org.apache.avalon.framework.service.ServiceException;
026import org.apache.avalon.framework.service.ServiceManager;
027import org.apache.avalon.framework.service.Serviceable;
028import org.apache.cocoon.ProcessingException;
029import org.apache.commons.lang3.StringUtils;
030
031import org.ametys.core.observation.Event;
032import org.ametys.core.observation.ObservationManager;
033import org.ametys.core.right.RightManager;
034import org.ametys.core.right.RightManager.RightResult;
035import org.ametys.core.ui.Callable;
036import org.ametys.core.user.CurrentUserProvider;
037import org.ametys.plugins.forms.FormEvents;
038import org.ametys.plugins.forms.actions.GetFormEntriesAction;
039import org.ametys.plugins.forms.helper.LimitedEntriesHelper;
040import org.ametys.plugins.forms.question.FormQuestionType;
041import org.ametys.plugins.forms.question.types.ChoicesListQuestionType;
042import org.ametys.plugins.forms.question.types.FileQuestionType;
043import org.ametys.plugins.forms.question.types.MatrixQuestionType;
044import org.ametys.plugins.forms.question.types.RichTextQuestionType;
045import org.ametys.plugins.forms.repository.Form;
046import org.ametys.plugins.forms.repository.FormEntry;
047import org.ametys.plugins.forms.repository.FormQuestion;
048import org.ametys.plugins.repository.AmetysObjectResolver;
049import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
050import org.ametys.plugins.repository.UnknownAmetysObjectException;
051import org.ametys.runtime.i18n.I18nizableText;
052import org.ametys.runtime.model.DefinitionContext;
053import org.ametys.runtime.model.Model;
054import org.ametys.runtime.model.View;
055import org.ametys.runtime.model.type.ModelItemTypeConstants;
056import org.ametys.runtime.plugin.component.AbstractLogEnabled;
057import org.ametys.web.parameters.ParametersManager;
058
059/**
060 * Form entry DAO.
061 */
062public class FormEntryDAO extends AbstractLogEnabled implements Serviceable, Component
063{
064    /** The Avalon role name. */
065    public static final String ROLE = FormEntryDAO.class.getName();
066
067    /** Name for entries root jcr node */
068    public static final String ENTRIES_ROOT = "ametys-internal:form-entries";
069    
070    /** The right id to consult form entries */
071    public static final String HANDLE_FORMS_ENTRIES_RIGHT_ID = "Form_Entries_Rights_Data";
072    
073    /** The right id to delete form entries */
074    public static final String DELETE_FORMS_ENTRIES_RIGHT_ID = "Runtime_Rights_Forms_Entry_Delete";
075    
076    /** Ametys object resolver. */
077    protected AmetysObjectResolver _resolver;
078    /** The parameters manager */
079    protected ParametersManager _parametersManager;
080    /** Observer manager. */
081    protected ObservationManager _observationManager;
082    /** The current user provider. */
083    protected CurrentUserProvider _currentUserProvider;
084    /** The handling limited entries helper */
085    protected LimitedEntriesHelper _handleLimitedEntriesHelper;
086    /** The rights manager */
087    protected RightManager _rightManager;
088    
089    @Override
090    public void service(ServiceManager serviceManager) throws ServiceException
091    {
092        _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
093        _parametersManager = (ParametersManager) serviceManager.lookup(ParametersManager.ROLE);
094        _observationManager = (ObservationManager) serviceManager.lookup(ObservationManager.ROLE);
095        _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE);
096        _handleLimitedEntriesHelper = (LimitedEntriesHelper) serviceManager.lookup(LimitedEntriesHelper.ROLE);
097        _rightManager = (RightManager) serviceManager.lookup(RightManager.ROLE);
098    }
099    
100    /**
101     * Gets properties of a form entry
102     * @param id The id of the form entry
103     * @return The properties
104     */
105    @Callable
106    public Map<String, Object> getFormEntryProperties (String id)
107    {
108        try
109        {
110            FormEntry entry = _resolver.resolveById(id);
111            return getFormEntryProperties(entry);
112        }
113        catch (UnknownAmetysObjectException e)
114        {
115            getLogger().warn("Can't find entry with id: {}. It probably has just been deleted", id, e);
116            Map<String, Object> infos = new HashMap<>();
117            infos.put("id", id);
118            return infos;
119        }
120    }
121    
122    /**
123     * Gets properties of a form entry
124     * @param entry The form entry
125     * @return The properties
126     */
127    public Map<String, Object> getFormEntryProperties (FormEntry entry)
128    {
129        Map<String, Object> properties = new HashMap<>();
130        
131        properties.put("id", entry.getId());
132        properties.put("formId", entry.getForm().getId());
133        
134        try
135        {
136            properties.put("path", entry.getNode().getPath());
137            properties.put("uuid", entry.getNode().getIdentifier());
138        }
139        catch (Exception e) 
140        {
141            getLogger().error("Can't have JCR property for form entry id '{}'.", entry.getId(), e);
142        }
143        
144        return properties;
145    }
146    
147    /**
148     * Creates a {@link FormEntry}.
149     * @param form The parent form
150     * @return return the form entry
151     */
152    public FormEntry createEntry(Form form)
153    {
154        ModifiableTraversableAmetysObject entriesRoot;
155        if (form.hasChild(ENTRIES_ROOT))
156        {
157            entriesRoot = form.getChild(ENTRIES_ROOT);
158        }
159        else
160        {
161            entriesRoot = form.createChild(ENTRIES_ROOT, "ametys:collection");
162        }
163        // Find unique name
164        String originalName = "entry";
165        String uniqueName = originalName + "-1";
166        int index = 2;
167        while (entriesRoot.hasChild(uniqueName))
168        {
169            uniqueName = originalName + "-" + (index++);
170        }
171        FormEntry entry =  (FormEntry) entriesRoot.createChild(uniqueName, "ametys:form-entry");
172        
173        Map<String, Object> eventParams = new HashMap<>();
174        eventParams.put("form", form);
175        _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _currentUserProvider.getUser(), eventParams));
176        
177        return entry;
178    }
179    
180    /**
181     * Get the columns information of a form
182     * @param formId the identifier of form
183     * @return The columns
184     * @throws IllegalArgumentException If an error occurred
185     * @throws ProcessingException If an error occurred
186     */
187    @Callable
188    public Map<String, Object>  getColumns (String formId) throws ProcessingException, IllegalArgumentException
189    {
190        Map<String, Object> result = new HashMap<>();
191        
192        Form form = _resolver.resolveById(formId);
193        List<FormEntry> entries = form.getEntries();
194        if (entries.isEmpty())
195        {
196            throw new IllegalAccessError("Can't call getColumns method because the form with id '" + formId + "' has no entries");
197        }
198        
199        FormEntry entry = form.getEntries().get(0); //Get first form entry just for the model
200        Collection< ? extends Model> model = entry.getModel();
201        Map<String, Object> json = View.of(model).toJSON(DefinitionContext.newInstance());
202        result.put("criteria", json);
203        
204        List<Map<String, Object>> columns = new ArrayList<>();
205        columns.add(
206            Map.of("name", FormEntry.ATTRIBUTE_ID,
207                    "label", new I18nizableText("plugin.forms", "PLUGIN_FORMS_MODEL_ITEM_ID_LABEL"),
208                    "type", ModelItemTypeConstants.LONG_TYPE_ID,
209                    "id", form.getId() + FormEntry.ATTRIBUTE_ID,
210                    "width", 80
211             )
212        );
213        
214        columns.add(
215            Map.of("name", FormEntry.ATTRIBUTE_USER,
216                    "label", new I18nizableText("plugin.forms", "PLUGIN_FORMS_MODEL_ITEM_USER_LABEL"),
217                    "type", org.ametys.cms.data.type.ModelItemTypeConstants.USER_ELEMENT_TYPE_ID,
218                    "id", form.getId() + FormEntry.ATTRIBUTE_USER,
219                    "width", 150
220             )
221        );
222        
223        columns.add(
224            Map.of("name", FormEntry.ATTRIBUTE_SUBMIT_DATE,
225                    "label", new I18nizableText("plugin.forms", "PLUGIN_FORMS_MODEL_ITEM_SUBMISSION_DATE_LABEL"),
226                    "type", ModelItemTypeConstants.DATETIME_TYPE_ID,
227                    "id", form.getId() + FormEntry.ATTRIBUTE_SUBMIT_DATE,
228                    "width", 150
229             )
230        );
231        
232        @SuppressWarnings("unchecked")
233        Map<String, Object> elements = (Map<String, Object>) json.get("elements");
234        for (String id : elements.keySet())
235        {
236            if (!id.equals(FormEntry.ATTRIBUTE_IP) 
237                 && !id.equals(FormEntry.ATTRIBUTE_ACTIVE) 
238                 && !id.equals(FormEntry.ATTRIBUTE_SUBMIT_DATE) 
239                 && !id.equals(FormEntry.ATTRIBUTE_ID) 
240                 && !id.equals(FormEntry.ATTRIBUTE_USER)
241                 && !id.startsWith(ChoicesListQuestionType.OTHER_PREFIX_DATA_NAME))
242            {
243                @SuppressWarnings("unchecked")
244                Map<String, Object> column = (Map<String, Object>) elements.get(id);
245                column.put("id", form.getId() + id);
246                
247                FormQuestion question = form.getQuestion(id);
248                if (question != null)
249                {
250                    FormQuestionType type = question.getType();
251                    String jsRenderer = type.getJSRenderer(question);
252                    if (StringUtils.isNotBlank(jsRenderer))
253                    {
254                        column.put("renderer", jsRenderer);
255                    }
256                    
257                    String jsConverter = type.getJSConverter(question);
258                    if (StringUtils.isNotBlank(jsConverter))
259                    {
260                        column.put("converter", jsConverter);
261                    }
262                }
263                
264                if (question != null && !_isSortable(entry, question))
265                {
266                    column.put("sortable", false);
267                }
268                
269                columns.add(column);
270            }
271        }
272        
273        if (form.isQueueEnabled())
274        {
275            columns.add(
276                Map.of("name", GetFormEntriesAction.QUEUE_STATUS,
277                        "label", new I18nizableText("plugin.forms", "PLUGINS_FORMS_QUEUE_STATUS_COLUMN_TITLE_LABEL"),
278                        "type", ModelItemTypeConstants.BOOLEAN_TYPE_ID,
279                        "id", GetFormEntriesAction.QUEUE_STATUS
280                 )
281            );
282        }
283        
284        if (form.hasWorkflow())
285        {
286            columns.add(
287                Map.of("name", GetFormEntriesAction.FORM_ENTRY_STATUS_ID,
288                       "label", new I18nizableText("plugin.forms", "PLUGINS_FORMS_WORKFLOW_TAB_STATUS_COLUMN_TITLE_LABEL"),
289                       "type", ModelItemTypeConstants.STRING_TYPE_ID,
290                       "id", GetFormEntriesAction.FORM_ENTRY_STATUS_ID
291                )
292            );
293        }
294        
295        columns.add(
296            Map.of("name", GetFormEntriesAction.FORM_ENTRY_ACTIVE,
297                    "label", new I18nizableText("plugin.forms", "PLUGINS_FORMS_ENTRY_ACTIVE_COLUMN_TITLE_LABEL"),
298                    "type", ModelItemTypeConstants.BOOLEAN_TYPE_ID,
299                    "id", GetFormEntriesAction.FORM_ENTRY_ACTIVE,
300                    "hidden", true
301             )
302        );
303        
304        result.put("columns", columns);
305
306        result.put("searchUrlPlugin", "forms");
307        result.put("searchUrl", "form/entries.json");
308        result.put("pageSize", 50);
309        return result;
310    }
311    
312    /**
313     * <code>true</code> if the column link to the question is sortable
314     * @param entry the entry for the model
315     * @param question the question
316     * @return <code>true</code> if the column link to the question is sortable
317     */
318    protected boolean _isSortable(FormEntry entry, FormQuestion question)
319    {
320        if (entry.isMultiple(question.getNameForForm()))
321        {
322            return false;
323        }
324        
325        FormQuestionType type = question.getType();
326        if (type instanceof MatrixQuestionType || type instanceof RichTextQuestionType || type instanceof FileQuestionType)
327        {
328            return false;
329        }
330        
331        return true;
332    }
333
334    /**
335     * Deletes a {@link FormEntry}.
336     * @param id The id of the form entry to delete
337     * @return The entry data
338     */
339    @Callable
340    public Map<String, String> deleteEntry (String id)
341    {
342        Map<String, String> result = new HashMap<>();
343        
344        FormEntry entry = _resolver.resolveById(id);
345        
346        if (_rightManager.currentUserHasRight(DELETE_FORMS_ENTRIES_RIGHT_ID, entry) != RightResult.RIGHT_ALLOW)
347        {
348            result.put("message", "not-allowed");
349            return result;
350        }
351        
352        _handleLimitedEntriesHelper.deactivateEntry(id);
353        
354        Form form = entry.getForm();
355        entry.remove();
356        
357        form.saveChanges();
358        
359        Map<String, Object> eventParams = new HashMap<>();
360        eventParams.put("form", form);
361        _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _currentUserProvider.getUser(), eventParams));
362        
363        result.put("entryId", id);
364        result.put("formId", form.getId());
365        result.put("hasEntries", String.valueOf(form.hasEntries()));
366        
367        return result;
368    }
369    
370    /**
371     * Delete all entries of a form
372     * @param id The id of the form
373     * @return the deleted entries data
374     */
375    @Callable
376    public Map<String, Object> clearEntries(String id)
377    {
378        Map<String, Object> result = new HashMap<>();
379        List<String> entryIds = new ArrayList<>();
380        Form form = _resolver.resolveById(id);
381        if (_rightManager.currentUserHasRight(DELETE_FORMS_ENTRIES_RIGHT_ID, form) != RightResult.RIGHT_ALLOW)
382        {
383            result.put("message", "not-allowed");
384            return result;
385        }
386        
387        for (FormEntry entry: form.getEntries())
388        {
389            entryIds.add(entry.getId());
390            entry.remove();
391        }
392        form.saveChanges();
393        Map<String, Object> eventParams = new HashMap<>();
394        eventParams.put("form", form);
395        _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _currentUserProvider.getUser(), eventParams));
396        
397        result.put("ids", entryIds);
398        result.put("formId", form.getId());
399        
400        return result;
401    }
402}