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