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}