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