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.actions;
017
018import java.util.ArrayList;
019import java.util.HashMap;
020import java.util.LinkedHashMap;
021import java.util.List;
022import java.util.Map;
023import java.util.stream.Collectors;
024
025import org.apache.avalon.framework.parameters.Parameters;
026import org.apache.avalon.framework.service.ServiceException;
027import org.apache.avalon.framework.service.ServiceManager;
028import org.apache.cocoon.ProcessingException;
029import org.apache.cocoon.acting.ServiceableAction;
030import org.apache.cocoon.environment.ObjectModelHelper;
031import org.apache.cocoon.environment.Redirector;
032import org.apache.cocoon.environment.Request;
033import org.apache.cocoon.environment.SourceResolver;
034import org.apache.commons.lang3.StringUtils;
035
036import org.ametys.core.cocoon.JSonReader;
037import org.ametys.core.right.RightManager;
038import org.ametys.core.right.RightManager.RightResult;
039import org.ametys.core.user.User;
040import org.ametys.core.user.UserIdentity;
041import org.ametys.core.user.UserManager;
042import org.ametys.core.util.I18nUtils;
043import org.ametys.core.util.JSONUtils;
044import org.ametys.plugins.core.user.UserHelper;
045import org.ametys.plugins.forms.dao.FormEntryDAO;
046import org.ametys.plugins.forms.helper.LimitedEntriesHelper;
047import org.ametys.plugins.forms.question.types.MatrixQuestionType;
048import org.ametys.plugins.forms.repository.Form;
049import org.ametys.plugins.forms.repository.FormEntry;
050import org.ametys.plugins.forms.repository.FormQuestion;
051import org.ametys.plugins.repository.AmetysObjectIterable;
052import org.ametys.plugins.repository.AmetysObjectResolver;
053import org.ametys.plugins.workflow.support.WorkflowProvider;
054import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow;
055import org.ametys.runtime.i18n.I18nizableText;
056import org.ametys.runtime.model.Model;
057import org.ametys.runtime.model.ModelItem;
058import org.ametys.runtime.model.type.DataContext;
059import org.ametys.runtime.model.type.ElementType;
060
061import com.opensymphony.workflow.loader.StepDescriptor;
062import com.opensymphony.workflow.loader.WorkflowDescriptor;
063import com.opensymphony.workflow.spi.Step;
064
065/**
066 * Get the submitted entries of a form
067 *
068 */
069public class GetFormEntriesAction extends ServiceableAction
070{
071    /** The id of the column for entry status */
072    public static final String FORM_ENTRY_STATUS_ID = "workflowStatus";
073    
074    /** Constant for whether an entry is in a queue or not */
075    public static final String QUEUE_STATUS = "queue-status";
076    
077    /** The id of the column for entry active or not */
078    public static final String FORM_ENTRY_ACTIVE = "active";
079    
080    /** The ametys object resolver. */
081    protected AmetysObjectResolver _resolver;
082    
083    /** The form entry DAO */
084    protected FormEntryDAO _formEntryDAO;
085    
086    /** The handle limited entries helper */
087    protected LimitedEntriesHelper _handleLimitedEntriesHelper;
088    
089    /** The user helper */
090    protected UserHelper _userHelper;
091    
092    /** The user manager */
093    protected UserManager _userManager;
094    
095    /** The workflow provider */
096    protected WorkflowProvider _workflowProvider;
097    
098    /** The right manager */
099    protected RightManager _rightManager;
100    
101    /** The json utils */
102    protected JSONUtils _jsonUtils;
103    
104    /** The I18n utils */
105    protected I18nUtils _i18nUtils;
106    
107    @Override
108    public void service(ServiceManager smanager) throws ServiceException
109    {
110        super.service(smanager);
111        _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE);
112        _formEntryDAO = (FormEntryDAO) smanager.lookup(FormEntryDAO.ROLE);
113        _userHelper = (UserHelper) smanager.lookup(UserHelper.ROLE);
114        _userManager = (UserManager) smanager.lookup(UserManager.ROLE);
115        _workflowProvider = (WorkflowProvider) smanager.lookup(WorkflowProvider.ROLE);
116        _handleLimitedEntriesHelper = (LimitedEntriesHelper) smanager.lookup(LimitedEntriesHelper.ROLE);
117        _rightManager = (RightManager) smanager.lookup(RightManager.ROLE);
118        _jsonUtils = (JSONUtils) smanager.lookup(JSONUtils.ROLE);
119        _i18nUtils = (I18nUtils) smanager.lookup(I18nUtils.ROLE);
120    }
121    
122    @Override
123    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
124    {
125        @SuppressWarnings("unchecked")
126        Map<String, Object> jsParameters = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT);
127        
128        String formId = (String) jsParameters.get("formId");
129        Form form = _resolver.resolveById(formId);
130        if (form == null)
131        {
132            throw new ProcessingException("The form of ID '" + formId + " can't be found.");
133        }
134        
135        if (_rightManager.currentUserHasRight(FormEntryDAO.HANDLE_FORMS_ENTRIES_RIGHT_ID, form) != RightResult.RIGHT_ALLOW)
136        {
137            throw new IllegalAccessError("Can't export form data without convenient right");
138        }
139        
140        Integer offset = _getIntValue(jsParameters, "start", 0);
141        Integer limit = _getIntValue(jsParameters, "limit", Integer.MAX_VALUE);
142
143        String sortInfo = (String) jsParameters.get("sort");
144        String groupInfo = (String) jsParameters.get("group");
145        List<Sort> sorts = _getSorts(formId, sortInfo, groupInfo);
146        
147        Map<String, Object> matrixLabels = _getMatrixInfos(form);
148        
149        Map<String, Object> result = new HashMap<>();
150        List<Map<String, Object>> entries2json = new ArrayList<>();
151        try
152        {
153            List<FormEntry> entries = _getEntries(form, sorts);
154            
155            int totalSubmissions = entries.size();
156            result.put("total", totalSubmissions);
157            
158            int currentLimit = 0;
159            while (currentLimit < limit && offset < totalSubmissions)
160            {
161                FormEntry entry = entries.get(offset);
162                Model entryModel = (Model) (entry.getModel().toArray())[0];
163                Map<String, Object> entryData = new LinkedHashMap<>();
164                for (ModelItem modelItem : entryModel.getModelItems())
165                {
166                    String name = modelItem.getName();
167                    FormQuestion question = form.getQuestion(name);
168                    if (question != null)
169                    {
170                        Object value = question.getType().valueToJSONForClient(entry.getValue(name), question, entry, modelItem);
171                        if (value != null)
172                        {
173                            entryData.put(form.getId() + name, value);
174                        }
175                    }
176                    else
177                    {
178                        DataContext context = DataContext.newInstance()
179                                .withDataPath(modelItem.getPath())
180                                .withObjectId(entry.getId());
181                        Object value = ((ElementType) modelItem.getType()).valueToJSONForClient(entry.getValue(name), context);
182                        if (value != null)
183                        {
184                            entryData.put(form.getId() + name, value);
185                        }
186                    }
187                    
188                    if (matrixLabels.containsKey(name))
189                    {
190                        entryData.put(form.getId() + name + "matrice-labels", matrixLabels.get(name));
191                    }
192                }
193
194                entryData.put(form.getId() + FormEntry.ATTRIBUTE_USER, _userHelper.user2json(entry.getUser(), true));
195                
196                if (form.hasWorkflow())
197                {
198                    AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(entry);
199                    WorkflowDescriptor workflowDescriptor = workflow.getWorkflowDescriptor(form.getWorkflowName());
200                    Step currentStep = (Step) workflow.getCurrentSteps(entry.getWorkflowId()).iterator().next();
201                    
202                    StepDescriptor stepDescriptor = workflowDescriptor.getStep(currentStep.getStepId());
203                    I18nizableText workflowStepName = new I18nizableText("application", stepDescriptor.getName());
204                    entryData.put(FORM_ENTRY_STATUS_ID, workflowStepName);
205                }
206                
207                if (form.isQueueEnabled() && entry.isActive())
208                {
209                    entryData.put(QUEUE_STATUS, _handleLimitedEntriesHelper.isInQueue(entry));
210                }
211                
212                entryData.put(FORM_ENTRY_ACTIVE, entry.isActive());
213                entryData.put("entryId", entries.get(offset).getId());
214                entryData.put("type", "form-entry");
215                entryData.put("parentId", form.getId());
216                entryData.put("hasEntries", form.hasEntries());
217                entries2json.add(entryData);
218                currentLimit++;
219                offset++;
220            }
221        }
222        catch (Exception e)
223        {
224            getLogger().error("Failed to get entries for form '" + form.getId() + "'.", e);
225        }
226        
227        result.put("entries", entries2json);
228        
229        Request request = ObjectModelHelper.getRequest(objectModel);
230        request.setAttribute(JSonReader.OBJECT_TO_READ, result);
231        return EMPTY_MAP;
232    }
233
234    /**
235     * Get entries and sort
236     * @param form the form
237     * @param sorts the sorts
238     * @return the list of entries
239     */
240    protected List<FormEntry> _getEntries(Form form, List<Sort> sorts)
241    {
242        if (sorts.isEmpty())
243        {
244            return form.getEntries();
245        }
246        else
247        {
248            if (sorts.size() == 1 && ("user".equals(sorts.get(0).attributeName()) || FORM_ENTRY_STATUS_ID.equals(sorts.get(0).attributeName())))
249            {
250                String attributeName = sorts.get(0).attributeName();
251                String direction = sorts.get(0).direction();
252                if ("user".equals(attributeName))
253                {
254                    return form.getEntries()
255                            .stream()
256                            .sorted((e1, e2) -> "ascending".equals(direction) 
257                                    ? StringUtils.compare(_getUserSortedName(e1), _getUserSortedName(e2))
258                                            : StringUtils.compare(_getUserSortedName(e2), _getUserSortedName(e1))
259                                    )
260                            .collect(Collectors.toList());
261                }
262                else if (FORM_ENTRY_STATUS_ID.equals(attributeName))
263                {
264                    return form.getEntries()
265                            .stream()
266                            .sorted((e1, e2) -> "ascending".equals(direction) 
267                                    ? StringUtils.compare(_getWorkflowLabel(e1), _getWorkflowLabel(e2))
268                                            : StringUtils.compare(_getWorkflowLabel(e2), _getWorkflowLabel(e1))
269                                    )
270                            .collect(Collectors.toList());
271                }
272                
273                return form.getEntries();
274            }
275            else
276            {
277                String uuid = StringUtils.substringAfter(form.getId(), "://");
278                String xpathQuery = "//element(*, ametys:form)[@jcr:uuid = '" + uuid + "']//element(*, ametys:form-entry)";
279                String sortsAsString = "";
280                for (Sort sort : sorts)
281                {
282                    if (StringUtils.isNotBlank(sortsAsString))
283                    {
284                        sortsAsString += ", ";
285                    }
286                    
287                    sortsAsString += "@ametys:" + sort.attributeName + " " + sort.direction;
288                }
289                
290                if (StringUtils.isNotBlank(sortsAsString))
291                {
292                    xpathQuery += " order by " + sortsAsString; 
293                }
294                
295                AmetysObjectIterable<FormEntry> zoneItems = _resolver.query(xpathQuery);
296                
297                return zoneItems.stream()
298                        .collect(Collectors.toList());
299            }
300        }
301    }
302    
303    private String _getUserSortedName(FormEntry entry)
304    {
305        UserIdentity userId = entry.getUser();
306        if (userId != null)
307        {
308            User user = _userManager.getUser(userId);
309            return user != null ? user.getSortableName() : null;
310        }
311        
312        return null;
313    }
314    
315    private String _getWorkflowLabel(FormEntry entry)
316    {
317        Form form = entry.getForm();
318        if (form.hasWorkflow())
319        {
320            AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(entry);
321            WorkflowDescriptor workflowDescriptor = workflow.getWorkflowDescriptor(form.getWorkflowName());
322            Step currentStep = (Step) workflow.getCurrentSteps(entry.getWorkflowId()).iterator().next();
323            
324            StepDescriptor stepDescriptor = workflowDescriptor.getStep(currentStep.getStepId());
325            I18nizableText workflowStepName = new I18nizableText("application", stepDescriptor.getName());
326            return _i18nUtils.translate(workflowStepName);
327        }
328        
329        return null;
330    }
331    
332    /**
333     * Get sorts of search form entry
334     * @param formId the form id
335     * @param sortString the sort as string
336     * @param groupString the group as string
337     * @return the list of sort
338     */
339    protected List<Sort> _getSorts(String formId, String sortString, String groupString)
340    {
341        List<Sort> sort = new ArrayList<>();
342        
343        List<Object> sortList = new ArrayList<>(_jsonUtils.convertJsonToList(sortString));
344        if (StringUtils.isNotEmpty(groupString))
345        {
346            // Grouping will be treated server side as a sort. It just needs to be before all the sorters
347            sortList.add(0, _jsonUtils.convertJsonToMap(groupString));
348        }
349        
350        for (Object object : sortList)
351        {
352            if (object instanceof Map)
353            {
354                Map map = (Map) object;
355                String fieldId = (String) map.get("property");
356                boolean ascending = "ASC".equals(map.get("direction"));
357                
358                sort.add(new Sort(
359                    StringUtils.contains(fieldId, formId) ? StringUtils.substringAfter(fieldId, formId) : fieldId, 
360                    ascending ? "ascending" : "descending"
361                ));
362            }
363        }
364        
365        return sort;
366    }    
367    
368    /**
369     * Get informations of matrix questions
370     * @param form the form
371     * @return the map of informations
372     */
373    protected Map<String, Object> _getMatrixInfos(Form form)
374    {
375        Map<String, Object> matrixLabels = new HashMap<>();
376        List<FormQuestion> matrixQuestions = form.getQuestions()
377            .stream()
378            .filter(q -> q.getType() instanceof MatrixQuestionType)
379            .toList();
380        for (FormQuestion matrixQ : matrixQuestions)
381        {
382            MatrixQuestionType type = (MatrixQuestionType) matrixQ.getType();
383            Map<String, String> columns = type.getColumns(matrixQ);
384            Map<String, String> rows = type.getRows(matrixQ);
385            Map<String, Map<String, String>> matrixInfo = (columns == null || rows == null)
386                    ? Map.of()
387                    : Map.of(
388                        "columns", type.getColumns(matrixQ),
389                        "rows", type.getRows(matrixQ)
390                      );
391            matrixLabels.put(matrixQ.getNameForForm(), matrixInfo);
392        }
393        return matrixLabels;
394    }
395    
396    private int _getIntValue(Map<String, Object> values, String key, int defaultValue)
397    {
398        if (values.containsKey(key))
399        {
400            return Integer.valueOf(values.get(key).toString()).intValue();
401        }
402        
403        return defaultValue;
404    }
405    
406    private record Sort(String attributeName, String direction) { /* */ }
407}