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.dao; 017 018import java.util.ArrayList; 019import java.util.Comparator; 020import java.util.HashMap; 021import java.util.List; 022import java.util.Map; 023import java.util.Objects; 024import java.util.Optional; 025import java.util.Set; 026import java.util.stream.Collectors; 027 028import javax.jcr.RepositoryException; 029 030import org.apache.avalon.framework.component.Component; 031import org.apache.avalon.framework.service.ServiceException; 032import org.apache.avalon.framework.service.ServiceManager; 033import org.apache.avalon.framework.service.Serviceable; 034import org.apache.cocoon.ProcessingException; 035import org.apache.commons.lang3.StringUtils; 036 037import org.ametys.cms.search.model.SystemPropertyExtensionPoint; 038import org.ametys.cms.search.ui.model.SearchUIColumn; 039import org.ametys.cms.search.ui.model.SearchUIColumnHelper; 040import org.ametys.core.observation.Event; 041import org.ametys.core.observation.ObservationManager; 042import org.ametys.core.right.RightManager; 043import org.ametys.core.right.RightManager.RightResult; 044import org.ametys.core.ui.Callable; 045import org.ametys.core.user.CurrentUserProvider; 046import org.ametys.core.user.User; 047import org.ametys.core.user.UserIdentity; 048import org.ametys.core.user.UserManager; 049import org.ametys.plugins.forms.FormEvents; 050import org.ametys.plugins.forms.actions.GetFormEntriesAction; 051import org.ametys.plugins.forms.helper.FormElementDefinitionHelper; 052import org.ametys.plugins.forms.helper.LimitedEntriesHelper; 053import org.ametys.plugins.forms.question.FormQuestionType; 054import org.ametys.plugins.forms.question.types.ChoicesListQuestionType; 055import org.ametys.plugins.forms.question.types.FileQuestionType; 056import org.ametys.plugins.forms.question.types.MatrixQuestionType; 057import org.ametys.plugins.forms.question.types.MultipleAwareFormQuestionType; 058import org.ametys.plugins.forms.question.types.RestrictiveQuestionType; 059import org.ametys.plugins.forms.question.types.RichTextQuestionType; 060import org.ametys.plugins.forms.repository.Form; 061import org.ametys.plugins.forms.repository.FormEntry; 062import org.ametys.plugins.forms.repository.FormQuestion; 063import org.ametys.plugins.forms.rights.FormsDirectoryRightAssignmentContext; 064import org.ametys.plugins.repository.AmetysObject; 065import org.ametys.plugins.repository.AmetysObjectIterable; 066import org.ametys.plugins.repository.AmetysObjectResolver; 067import org.ametys.plugins.repository.AmetysRepositoryException; 068import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 069import org.ametys.plugins.repository.UnknownAmetysObjectException; 070import org.ametys.plugins.repository.jcr.JCRAmetysObject; 071import org.ametys.plugins.repository.query.expression.AndExpression; 072import org.ametys.plugins.repository.query.expression.BooleanExpression; 073import org.ametys.plugins.repository.query.expression.Expression; 074import org.ametys.plugins.workflow.support.WorkflowProvider; 075import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow; 076import org.ametys.runtime.i18n.I18nizableText; 077import org.ametys.runtime.model.DefinitionContext; 078import org.ametys.runtime.model.Model; 079import org.ametys.runtime.model.ModelItem; 080import org.ametys.runtime.model.type.ModelItemTypeConstants; 081import org.ametys.runtime.plugin.component.AbstractLogEnabled; 082import org.ametys.web.parameters.ParametersManager; 083 084import com.opensymphony.workflow.spi.Step; 085 086/** 087 * Form entry DAO. 088 */ 089public class FormEntryDAO extends AbstractLogEnabled implements Serviceable, Component 090{ 091 /** The Avalon role name. */ 092 public static final String ROLE = FormEntryDAO.class.getName(); 093 094 /** Name for entries root jcr node */ 095 public static final String ENTRIES_ROOT = "ametys-internal:form-entries"; 096 097 /** The right id to consult form entries */ 098 public static final String HANDLE_FORMS_ENTRIES_RIGHT_ID = "Form_Entries_Rights_Data"; 099 100 /** The right id to delete form entries */ 101 public static final String DELETE_FORMS_ENTRIES_RIGHT_ID = "Runtime_Rights_Forms_Entry_Delete"; 102 103 /** Ametys object resolver. */ 104 protected AmetysObjectResolver _resolver; 105 /** The parameters manager */ 106 protected ParametersManager _parametersManager; 107 /** Observer manager. */ 108 protected ObservationManager _observationManager; 109 /** The current user provider. */ 110 protected CurrentUserProvider _currentUserProvider; 111 /** The handling limited entries helper */ 112 protected LimitedEntriesHelper _handleLimitedEntriesHelper; 113 /** The rights manager */ 114 protected RightManager _rightManager; 115 /** The current user provider */ 116 protected WorkflowProvider _workflowProvider; 117 /** The user manager */ 118 protected UserManager _userManager; 119 /** The system property extension point */ 120 protected SystemPropertyExtensionPoint _systemPropertyEP; 121 122 @Override 123 public void service(ServiceManager serviceManager) throws ServiceException 124 { 125 _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE); 126 _parametersManager = (ParametersManager) serviceManager.lookup(ParametersManager.ROLE); 127 _observationManager = (ObservationManager) serviceManager.lookup(ObservationManager.ROLE); 128 _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE); 129 _handleLimitedEntriesHelper = (LimitedEntriesHelper) serviceManager.lookup(LimitedEntriesHelper.ROLE); 130 _rightManager = (RightManager) serviceManager.lookup(RightManager.ROLE); 131 _workflowProvider = (WorkflowProvider) serviceManager.lookup(WorkflowProvider.ROLE); 132 _userManager = (UserManager) serviceManager.lookup(UserManager.ROLE); 133 _systemPropertyEP = (SystemPropertyExtensionPoint) serviceManager.lookup(SystemPropertyExtensionPoint.ROLE); 134 } 135 136 /** 137 * Check if a user have handle data right on a form element as ametys object 138 * @param userIdentity the user 139 * @param formElement the form element 140 * @return true if the user handle data right for a form element 141 */ 142 public boolean hasHandleDataRightOnForm(UserIdentity userIdentity, AmetysObject formElement) 143 { 144 return _rightManager.hasRight(userIdentity, HANDLE_FORMS_ENTRIES_RIGHT_ID, formElement) == RightResult.RIGHT_ALLOW; 145 } 146 147 /** 148 * Check handle data right for a form element as ametys object 149 * @param formElement the form element as ametys object 150 */ 151 public void checkHandleDataRight(AmetysObject formElement) 152 { 153 UserIdentity user = _currentUserProvider.getUser(); 154 if (!hasHandleDataRightOnForm(user, formElement)) 155 { 156 throw new IllegalAccessError("User '" + user + "' tried to handle form data without convenient right [" + HANDLE_FORMS_ENTRIES_RIGHT_ID + "]"); 157 } 158 } 159 160 /** 161 * Gets properties of a form entry 162 * @param id The id of the form entry 163 * @return The properties 164 */ 165 @Callable 166 public Map<String, Object> getFormEntryProperties (String id) 167 { 168 try 169 { 170 FormEntry entry = _resolver.resolveById(id); 171 return getFormEntryProperties(entry); 172 } 173 catch (UnknownAmetysObjectException e) 174 { 175 getLogger().warn("Can't find entry with id: {}. It probably has just been deleted", id, e); 176 Map<String, Object> infos = new HashMap<>(); 177 infos.put("id", id); 178 return infos; 179 } 180 } 181 182 /** 183 * Gets properties of a form entry 184 * @param entry The form entry 185 * @return The properties 186 */ 187 public Map<String, Object> getFormEntryProperties (FormEntry entry) 188 { 189 Map<String, Object> properties = new HashMap<>(); 190 191 properties.put("id", entry.getId()); 192 properties.put("formId", entry.getForm().getId()); 193 properties.put("rights", _getUserRights(entry)); 194 195 return properties; 196 } 197 198 /** 199 * Get user rights for the given form entry 200 * @param entry the form entry 201 * @return the set of rights 202 */ 203 protected Set<String> _getUserRights (FormEntry entry) 204 { 205 UserIdentity user = _currentUserProvider.getUser(); 206 return _rightManager.getUserRights(user, entry); 207 } 208 209 /** 210 * Creates a {@link FormEntry}. 211 * @param form The parent form 212 * @return return the form entry 213 */ 214 public FormEntry createEntry(Form form) 215 { 216 ModifiableTraversableAmetysObject entriesRoot; 217 if (form.hasChild(ENTRIES_ROOT)) 218 { 219 entriesRoot = form.getChild(ENTRIES_ROOT); 220 } 221 else 222 { 223 entriesRoot = form.createChild(ENTRIES_ROOT, "ametys:collection"); 224 } 225 // Find unique name 226 String originalName = "entry"; 227 String uniqueName = originalName + "-1"; 228 int index = 2; 229 while (entriesRoot.hasChild(uniqueName)) 230 { 231 uniqueName = originalName + "-" + (index++); 232 } 233 FormEntry entry = (FormEntry) entriesRoot.createChild(uniqueName, "ametys:form-entry"); 234 235 Map<String, Object> eventParams = new HashMap<>(); 236 eventParams.put("form", form); 237 _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _currentUserProvider.getUser(), eventParams)); 238 239 return entry; 240 } 241 242 /** 243 * Get the search model configuration to search form entries 244 * @param formId the identifier of form 245 * @return The search model configuration 246 * @throws ProcessingException If an error occurred 247 */ 248 @Callable 249 public Map<String, Object> getSearchModelConfiguration (String formId) throws ProcessingException 250 { 251 Map<String, Object> result = new HashMap<>(); 252 253 Form form = _resolver.resolveById(formId); 254 result.put("criteria", _getCriteria(form)); 255 result.put("columns", _getColumns(form)); 256 257 result.put("searchUrlPlugin", "forms"); 258 result.put("searchUrl", "form/entries.json"); 259 result.put("pageSize", 50); 260 return result; 261 } 262 263 /** 264 * Get criteria to search form entries 265 * @param form the form 266 * @return the criteria as JSON 267 */ 268 protected Map<String, Object> _getCriteria(Form form) 269 { 270 // Currently, return no criteria for search entries tool 271 return Map.of(); 272 } 273 274 /** 275 * Get the columns for search form entries 276 * @param form the form 277 * @return the columns as JSON 278 * @throws ProcessingException if an error occurred 279 */ 280 protected List<Map<String, Object>> _getColumns(Form form) throws ProcessingException 281 { 282 List<SearchUIColumn> columns = new ArrayList<>(); 283 284 Model formEntryModel = getFormEntryModel(form); 285 286 ModelItem idAttribute = formEntryModel.getModelItem(FormEntry.ATTRIBUTE_ID); 287 SearchUIColumn idColumn = SearchUIColumnHelper.createModelItemColumn(idAttribute); 288 idColumn.setWidth(80); 289 columns.add(idColumn); 290 291 ModelItem userAttribute = formEntryModel.getModelItem(FormEntry.ATTRIBUTE_USER); 292 SearchUIColumn userColumn = SearchUIColumnHelper.createModelItemColumn(userAttribute); 293 userColumn.setWidth(150); 294 columns.add(userColumn); 295 296 ModelItem submitDateAttribute = formEntryModel.getModelItem(FormEntry.ATTRIBUTE_SUBMIT_DATE); 297 SearchUIColumn submitDateColumn = SearchUIColumnHelper.createModelItemColumn(submitDateAttribute); 298 submitDateColumn.setWidth(150); 299 columns.add(submitDateColumn); 300 301 for (ModelItem modelItem : formEntryModel.getModelItems()) 302 { 303 String modelItemName = modelItem.getName(); 304 if (!modelItemName.equals(FormEntry.ATTRIBUTE_IP) 305 && !modelItemName.equals(FormEntry.ATTRIBUTE_ACTIVE) 306 && !modelItemName.equals(FormEntry.ATTRIBUTE_SUBMIT_DATE) 307 && !modelItemName.equals(FormEntry.ATTRIBUTE_ID) 308 && !modelItemName.equals(FormEntry.ATTRIBUTE_USER) 309 && !modelItemName.startsWith(ChoicesListQuestionType.OTHER_PREFIX_DATA_NAME)) 310 { 311 SearchUIColumn<? extends ModelItem> column = SearchUIColumnHelper.createModelItemColumn(modelItem); 312 313 FormQuestion question = form.getQuestion(modelItemName); 314 if (question != null) 315 { 316 FormQuestionType type = question.getType(); 317 318 column.setRenderer(Optional.ofNullable(type.getJSRenderer(question)) 319 .filter(StringUtils::isNotBlank)); 320 321 column.setConverter(Optional.ofNullable(type.getJSConverter(question)) 322 .filter(StringUtils::isNotBlank)); 323 324 column.setSortable(_isSortable(question)); 325 } 326 327 columns.add(column); 328 } 329 } 330 331 List<Map<String, Object>> columnsInfo = new ArrayList<>(); 332 DefinitionContext definitionContext = DefinitionContext.newInstance(); 333 for (SearchUIColumn column : columns) 334 { 335 columnsInfo.add(column.toJSON(definitionContext)); 336 } 337 338 if (form.isQueueEnabled()) 339 { 340 columnsInfo.add( 341 Map.of("name", FormEntry.SYSTEM_ATTRIBUTE_PREFIX + GetFormEntriesAction.QUEUE_STATUS, 342 "label", new I18nizableText("plugin.forms", "PLUGINS_FORMS_QUEUE_STATUS_COLUMN_TITLE_LABEL"), 343 "type", ModelItemTypeConstants.BOOLEAN_TYPE_ID, 344 "path", FormEntry.SYSTEM_ATTRIBUTE_PREFIX + GetFormEntriesAction.QUEUE_STATUS 345 ) 346 ); 347 } 348 349 columnsInfo.add( 350 Map.of("name", FormEntry.SYSTEM_ATTRIBUTE_PREFIX + GetFormEntriesAction.FORM_ENTRY_ACTIVE, 351 "label", new I18nizableText("plugin.forms", "PLUGINS_FORMS_ENTRY_ACTIVE_COLUMN_TITLE_LABEL"), 352 "type", ModelItemTypeConstants.BOOLEAN_TYPE_ID, 353 "path", FormEntry.SYSTEM_ATTRIBUTE_PREFIX + GetFormEntriesAction.FORM_ENTRY_ACTIVE, 354 "hidden", true 355 ) 356 ); 357 358 return columnsInfo; 359 } 360 361 /** 362 * <code>true</code> if the column link to the question is sortable 363 * @param question the question 364 * @return <code>true</code> if the column link to the question is sortable 365 */ 366 protected boolean _isSortable(FormQuestion question) 367 { 368 FormQuestionType type = question.getType(); 369 if (type instanceof MultipleAwareFormQuestionType multipleType && multipleType.isMultiple(question)) 370 { 371 return false; 372 } 373 374 if (type instanceof MatrixQuestionType || type instanceof RichTextQuestionType || type instanceof FileQuestionType) 375 { 376 return false; 377 } 378 379 return true; 380 } 381 382 /** 383 * Deletes a {@link FormEntry}. 384 * @param id The id of the form entry to delete 385 * @return The entry data 386 */ 387 @Callable 388 public Map<String, String> deleteEntry (String id) 389 { 390 Map<String, String> result = new HashMap<>(); 391 392 FormEntry entry = _resolver.resolveById(id); 393 394 if (_rightManager.currentUserHasRight(DELETE_FORMS_ENTRIES_RIGHT_ID, entry) != RightResult.RIGHT_ALLOW) 395 { 396 throw new IllegalAccessError("User '" + _currentUserProvider.getUser() + "' tried to delete entries without convenient right [" + DELETE_FORMS_ENTRIES_RIGHT_ID + "]"); 397 } 398 399 _handleLimitedEntriesHelper.deactivateEntry(id); 400 401 Form form = entry.getForm(); 402 entry.remove(); 403 404 form.saveChanges(); 405 406 Map<String, Object> eventParams = new HashMap<>(); 407 eventParams.put("form", form); 408 _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _currentUserProvider.getUser(), eventParams)); 409 410 result.put("entryId", id); 411 result.put("formId", form.getId()); 412 result.put("hasEntries", String.valueOf(form.hasEntries())); 413 414 return result; 415 } 416 417 /** 418 * Delete all entries of a form 419 * @param id The id of the form 420 * @return the deleted entries data 421 */ 422 @Callable 423 public Map<String, Object> clearEntries(String id) 424 { 425 Map<String, Object> result = new HashMap<>(); 426 List<String> entryIds = new ArrayList<>(); 427 Form form = _resolver.resolveById(id); 428 if (_rightManager.currentUserHasRight(DELETE_FORMS_ENTRIES_RIGHT_ID, form) != RightResult.RIGHT_ALLOW) 429 { 430 throw new IllegalAccessError("User '" + _currentUserProvider.getUser() + "' tried to delete entries without convenient right [" + DELETE_FORMS_ENTRIES_RIGHT_ID + "]"); 431 } 432 433 for (FormEntry entry: form.getEntries()) 434 { 435 entryIds.add(entry.getId()); 436 entry.remove(); 437 } 438 form.saveChanges(); 439 Map<String, Object> eventParams = new HashMap<>(); 440 eventParams.put("form", form); 441 _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _currentUserProvider.getUser(), eventParams)); 442 443 result.put("ids", entryIds); 444 result.put("formId", form.getId()); 445 446 return result; 447 } 448 449 /** 450 * Retrieves the current step id of the form entry 451 * @param entry The form entry 452 * @return the current step id 453 * @throws AmetysRepositoryException if an error occurs. 454 */ 455 public Long getCurrentStepId(FormEntry entry) throws AmetysRepositoryException 456 { 457 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(entry); 458 try 459 { 460 Step currentStep = (Step) workflow.getCurrentSteps(entry.getWorkflowId()).iterator().next(); 461 return Long.valueOf(currentStep.getStepId()); 462 } 463 catch (AmetysRepositoryException e) 464 { 465 return RestrictiveQuestionType.INITIAL_WORKFLOW_ID; // can occur when entry has just been created and workflow is not yet initialized 466 } 467 } 468 469 /** 470 * Get the form entry model 471 * @param form the form 472 * @return the form entry model 473 */ 474 public Model getFormEntryModel(Form form) 475 { 476 List<ModelItem> items = new ArrayList<>(); 477 for (FormQuestion question : form.getQuestions()) 478 { 479 FormQuestionType type = question.getType(); 480 if (!type.onlyForDisplay(question)) 481 { 482 Model entryModel = question.getType().getEntryModel(question); 483 for (ModelItem modelItem : entryModel.getModelItems()) 484 { 485 items.add(modelItem); 486 } 487 488 if (type instanceof ChoicesListQuestionType cLType) 489 { 490 ModelItem otherFieldModel = cLType.getOtherFieldModel(question); 491 if (otherFieldModel != null) 492 { 493 items.add(otherFieldModel); 494 } 495 } 496 } 497 } 498 499 items.add(FormElementDefinitionHelper.getElementDefinition(FormEntry.ATTRIBUTE_ID, ModelItemTypeConstants.LONG_TYPE_ID, "PLUGIN_FORMS_MODEL_ITEM_ID_LABEL", null, null)); 500 items.add(FormElementDefinitionHelper.getElementDefinition(FormEntry.ATTRIBUTE_USER, org.ametys.cms.data.type.ModelItemTypeConstants.USER_ELEMENT_TYPE_ID, "PLUGIN_FORMS_MODEL_ITEM_USER_LABEL", null, null)); 501 items.add(FormElementDefinitionHelper.getElementDefinition(FormEntry.ATTRIBUTE_ACTIVE, ModelItemTypeConstants.BOOLEAN_TYPE_ID, "PLUGIN_FORMS_MODEL_ITEM_ACTIVE_LABEL", null, null)); 502 items.add(FormElementDefinitionHelper.getElementDefinition(FormEntry.ATTRIBUTE_SUBMIT_DATE, ModelItemTypeConstants.DATETIME_TYPE_ID, "PLUGIN_FORMS_MODEL_ITEM_SUBMISSION_DATE_LABEL", null, null)); 503 items.add(FormElementDefinitionHelper.getElementDefinition(FormEntry.ATTRIBUTE_IP, ModelItemTypeConstants.STRING_TYPE_ID, "PLUGIN_FORMS_MODEL_ITEM_IP_LABEL", null, null)); 504 505 if (form.hasWorkflow()) 506 { 507 items.add(_systemPropertyEP.getExtension("workflowName")); 508 items.add(_systemPropertyEP.getExtension("workflowStep")); 509 } 510 511 return Model.of( 512 "form.entry.model.id", 513 "form.entry.model.family.id", 514 items.toArray(ModelItem[]::new) 515 ); 516 } 517 518 /** 519 * Get the form entries 520 * @param form the form 521 * @param onlyActiveEntries <code>true</code> to have only active entries 522 * @param sorts the list of sort 523 * @return the form entries 524 */ 525 public List<FormEntry> getFormEntries(Form form, boolean onlyActiveEntries, List<Sort> sorts) 526 { 527 return getFormEntries(form, onlyActiveEntries, null, sorts); 528 } 529 530 /** 531 * Get the form entries 532 * @param form the form 533 * @param onlyActiveEntries <code>true</code> to have only active entries 534 * @param additionalEntryFilterExpr the additional entry filter expression. Can be null. 535 * @param sorts the list of sort 536 * @return the form entries 537 */ 538 public List<FormEntry> getFormEntries(Form form, boolean onlyActiveEntries, Expression additionalEntryFilterExpr, List<Sort> sorts) 539 { 540 try 541 { 542 String uuid = ((JCRAmetysObject) form).getNode().getIdentifier(); 543 String xpathQuery = "//element(*, ametys:form)[@jcr:uuid = '" + uuid + "']//element(*, ametys:form-entry)"; 544 545 String entryFilterQuery = _getEntryFilterQuery(onlyActiveEntries, additionalEntryFilterExpr); 546 if (onlyActiveEntries || StringUtils.isNotBlank(entryFilterQuery)) 547 { 548 xpathQuery += "[" + entryFilterQuery + "]"; 549 } 550 551 String sortsAsString = ""; 552 for (Sort sort : sorts) 553 { 554 if (StringUtils.isNotBlank(sortsAsString)) 555 { 556 sortsAsString += ", "; 557 } 558 559 sortsAsString += "@ametys:" + sort.attributeName() + " " + sort.direction(); 560 } 561 562 if (StringUtils.isNotBlank(sortsAsString)) 563 { 564 xpathQuery += " order by " + sortsAsString; 565 } 566 567 AmetysObjectIterable<FormEntry> formEntries = _resolver.query(xpathQuery); 568 return formEntries.stream().collect(Collectors.toList()); 569 } 570 catch (RepositoryException e) 571 { 572 getLogger().error("An error occurred getting entries of form '" + form.getId() + "'"); 573 } 574 575 return List.of(); 576 } 577 578 private String _getEntryFilterQuery(boolean onlyActiveEntries, Expression additionalEntryFilterExpr) 579 { 580 List<Expression> expressions = new ArrayList<>(); 581 if (onlyActiveEntries) 582 { 583 expressions.add(new BooleanExpression(FormEntry.ATTRIBUTE_ACTIVE, true)); 584 } 585 586 if (additionalEntryFilterExpr != null) 587 { 588 expressions.add(additionalEntryFilterExpr); 589 } 590 591 return new AndExpression(expressions).build(); 592 } 593 594 /** 595 * Get all users who answer to the form as JSON 596 * @param formId the form id 597 * @return all users as JSON 598 */ 599 @Callable (rights = HANDLE_FORMS_ENTRIES_RIGHT_ID, paramIndex = 0, rightContext = FormsDirectoryRightAssignmentContext.ID) 600 public List<Map<String, String>> getFormEntriesUsers(String formId) 601 { 602 Form form = _resolver.resolveById(formId); 603 return getFormEntries(form, false, new ArrayList<>()).stream() 604 .map(FormEntry::getUser) 605 .distinct() 606 .map(_userManager::getUser) 607 .filter(Objects::nonNull) 608 .sorted(Comparator.comparing(User::getFullName, String.CASE_INSENSITIVE_ORDER)) 609 .map(u -> Map.of("text", u.getFullName(), "id", UserIdentity.userIdentityToString(u.getIdentity()))) 610 .toList(); 611 } 612 613 /** 614 * Record representing a sort with form attribute name and direction 615 * @param attributeName the attribute name 616 * @param direction the direction 617 */ 618 public record Sort(String attributeName, String direction) { /* */ } 619}