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