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.dao.FormEntryDAO.Sort;
045import org.ametys.plugins.forms.helper.LimitedEntriesHelper;
046import org.ametys.plugins.forms.question.types.MatrixQuestionType;
047import org.ametys.plugins.forms.repository.Form;
048import org.ametys.plugins.forms.repository.FormEntry;
049import org.ametys.plugins.forms.repository.FormQuestion;
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                return _formEntryDAO.getFormEntries(form, false, sorts);
266            }
267        }
268    }
269    
270    private String _getUserSortedName(FormEntry entry)
271    {
272        UserIdentity userId = entry.getUser();
273        if (userId != null)
274        {
275            User user = _userManager.getUser(userId);
276            return user != null ? user.getSortableName() : null;
277        }
278        
279        return null;
280    }
281    
282    private String _getWorkflowLabel(FormEntry entry)
283    {
284        Form form = entry.getForm();
285        if (form.hasWorkflow())
286        {
287            AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(entry);
288            WorkflowDescriptor workflowDescriptor = workflow.getWorkflowDescriptor(form.getWorkflowName());
289            Step currentStep = (Step) workflow.getCurrentSteps(entry.getWorkflowId()).iterator().next();
290            
291            StepDescriptor stepDescriptor = workflowDescriptor.getStep(currentStep.getStepId());
292            I18nizableText workflowStepName = new I18nizableText("application", stepDescriptor.getName());
293            return _i18nUtils.translate(workflowStepName);
294        }
295        
296        return null;
297    }
298    
299    /**
300     * Get sorts of search form entry
301     * @param formId the form id
302     * @param sortString the sort as string
303     * @param groupString the group as string
304     * @return the list of sort
305     */
306    protected List<Sort> _getSorts(String formId, String sortString, String groupString)
307    {
308        List<Sort> sort = new ArrayList<>();
309        
310        List<Object> sortList = new ArrayList<>(_jsonUtils.convertJsonToList(sortString));
311        if (StringUtils.isNotEmpty(groupString))
312        {
313            // Grouping will be treated server side as a sort. It just needs to be before all the sorters
314            sortList.add(0, _jsonUtils.convertJsonToMap(groupString));
315        }
316        
317        for (Object object : sortList)
318        {
319            if (object instanceof Map)
320            {
321                Map map = (Map) object;
322                String fieldId = (String) map.get("property");
323                boolean ascending = "ASC".equals(map.get("direction"));
324                
325                sort.add(new Sort(
326                    StringUtils.contains(fieldId, formId) ? StringUtils.substringAfter(fieldId, formId) : fieldId, 
327                    ascending ? "ascending" : "descending"
328                ));
329            }
330        }
331        
332        return sort;
333    }    
334    
335    /**
336     * Get informations of matrix questions
337     * @param form the form
338     * @return the map of informations
339     */
340    protected Map<String, Object> _getMatrixInfos(Form form)
341    {
342        Map<String, Object> matrixLabels = new HashMap<>();
343        List<FormQuestion> matrixQuestions = form.getQuestions()
344            .stream()
345            .filter(q -> q.getType() instanceof MatrixQuestionType)
346            .toList();
347        for (FormQuestion matrixQ : matrixQuestions)
348        {
349            MatrixQuestionType type = (MatrixQuestionType) matrixQ.getType();
350            Map<String, String> columns = type.getColumns(matrixQ);
351            Map<String, String> rows = type.getRows(matrixQ);
352            Map<String, Map<String, String>> matrixInfo = (columns == null || rows == null)
353                    ? Map.of()
354                    : Map.of(
355                        "columns", type.getColumns(matrixQ),
356                        "rows", type.getRows(matrixQ)
357                      );
358            matrixLabels.put(matrixQ.getNameForForm(), matrixInfo);
359        }
360        return matrixLabels;
361    }
362    
363    private int _getIntValue(Map<String, Object> values, String key, int defaultValue)
364    {
365        if (values.containsKey(key))
366        {
367            return Integer.valueOf(values.get(key).toString()).intValue();
368        }
369        
370        return defaultValue;
371    }
372}