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