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