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