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