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.time.ZonedDateTime; 019import java.util.ArrayList; 020import java.util.Comparator; 021import java.util.HashMap; 022import java.util.List; 023import java.util.Map; 024import java.util.Objects; 025import java.util.Optional; 026import java.util.Set; 027import java.util.stream.Collectors; 028 029import javax.jcr.RepositoryException; 030 031import org.apache.avalon.framework.component.Component; 032import org.apache.avalon.framework.service.ServiceException; 033import org.apache.avalon.framework.service.ServiceManager; 034import org.apache.avalon.framework.service.Serviceable; 035import org.apache.cocoon.ProcessingException; 036import org.apache.commons.lang3.StringUtils; 037import org.apache.jackrabbit.JcrConstants; 038 039import org.ametys.cms.fo.ForceDefaultRepositoryWorkspaceCallableDecorator; 040import org.ametys.cms.search.SearchResults; 041import org.ametys.cms.search.query.AndQuery; 042import org.ametys.cms.search.query.BooleanQuery; 043import org.ametys.cms.search.query.DateQuery; 044import org.ametys.cms.search.query.DocumentTypeQuery; 045import org.ametys.cms.search.query.NotQuery; 046import org.ametys.cms.search.query.OrQuery; 047import org.ametys.cms.search.query.Query; 048import org.ametys.cms.search.query.Query.Operator; 049import org.ametys.cms.search.query.UsersQuery; 050import org.ametys.cms.search.solr.SearcherFactory; 051import org.ametys.cms.search.solr.SearcherFactory.FacetDefinition; 052import org.ametys.cms.search.solr.SearcherFactory.Searcher; 053import org.ametys.cms.search.solr.SearcherFactory.SortDefinition; 054import org.ametys.cms.search.ui.model.SearchUIColumn; 055import org.ametys.cms.search.ui.model.SearchUIColumnHelper; 056import org.ametys.core.observation.Event; 057import org.ametys.core.observation.ObservationManager; 058import org.ametys.core.right.RightManager; 059import org.ametys.core.right.RightManager.RightResult; 060import org.ametys.core.ui.Callable; 061import org.ametys.core.user.CurrentUserProvider; 062import org.ametys.core.user.User; 063import org.ametys.core.user.UserIdentity; 064import org.ametys.core.user.UserManager; 065import org.ametys.plugins.forms.FormEntrySystemPropertyExtensionPoint; 066import org.ametys.plugins.forms.ObservationConstants; 067import org.ametys.plugins.forms.actions.GetFormEntriesAction; 068import org.ametys.plugins.forms.helper.FormElementDefinitionHelper; 069import org.ametys.plugins.forms.helper.FormEntriesSearchHelper; 070import org.ametys.plugins.forms.helper.FormEntriesSearchHelper.FormEntryColumn; 071import org.ametys.plugins.forms.helper.FormWorkflowHelper; 072import org.ametys.plugins.forms.helper.LimitedEntriesHelper; 073import org.ametys.plugins.forms.indexing.solr.SolrFormEntryIndexer; 074import org.ametys.plugins.forms.indexing.solr.query.FormQuery; 075import org.ametys.plugins.forms.question.FormQuestionType; 076import org.ametys.plugins.forms.question.types.RestrictiveAwareQuestionType; 077import org.ametys.plugins.forms.question.types.impl.ChoicesListQuestionType; 078import org.ametys.plugins.forms.question.types.impl.ComputedQuestionType; 079import org.ametys.plugins.forms.repository.Form; 080import org.ametys.plugins.forms.repository.FormEntry; 081import org.ametys.plugins.forms.repository.FormQuestion; 082import org.ametys.plugins.forms.rights.FormsDirectoryRightAssignmentContext; 083import org.ametys.plugins.repository.AmetysObject; 084import org.ametys.plugins.repository.AmetysObjectIterable; 085import org.ametys.plugins.repository.AmetysObjectResolver; 086import org.ametys.plugins.repository.AmetysRepositoryException; 087import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 088import org.ametys.plugins.repository.UnknownAmetysObjectException; 089import org.ametys.plugins.repository.query.QueryHelper; 090import org.ametys.plugins.repository.query.expression.AndExpression; 091import org.ametys.plugins.repository.query.expression.BooleanExpression; 092import org.ametys.plugins.repository.query.expression.Expression; 093import org.ametys.plugins.workflow.support.WorkflowProvider; 094import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow; 095import org.ametys.runtime.authentication.AccessDeniedException; 096import org.ametys.runtime.authentication.AuthorizationRequiredException; 097import org.ametys.runtime.i18n.I18nizableText; 098import org.ametys.runtime.model.DefinitionContext; 099import org.ametys.runtime.model.Model; 100import org.ametys.runtime.model.ModelItem; 101import org.ametys.runtime.model.View; 102import org.ametys.runtime.model.type.ModelItemTypeConstants; 103import org.ametys.runtime.plugin.component.AbstractLogEnabled; 104import org.ametys.web.parameters.ParametersManager; 105import org.ametys.web.repository.site.Site; 106import org.ametys.web.search.query.SiteQuery; 107 108import com.opensymphony.workflow.spi.Step; 109 110/** 111 * Form entry DAO. 112 */ 113public class FormEntryDAO extends AbstractLogEnabled implements Serviceable, Component 114{ 115 /** The Avalon role name. */ 116 public static final String ROLE = FormEntryDAO.class.getName(); 117 118 /** Name for entries root jcr node */ 119 public static final String ENTRIES_ROOT = "ametys-internal:form-entries"; 120 121 /** The right id to consult form entries */ 122 public static final String HANDLE_FORMS_ENTRIES_RIGHT_ID = "Form_Entries_Rights_Data"; 123 124 /** The right id to delete form entries */ 125 public static final String DELETE_FORMS_ENTRIES_RIGHT_ID = "Runtime_Rights_Forms_Entry_Delete"; 126 127 /** Ametys object resolver. */ 128 protected AmetysObjectResolver _resolver; 129 /** The parameters manager */ 130 protected ParametersManager _parametersManager; 131 /** Observer manager. */ 132 protected ObservationManager _observationManager; 133 /** The current user provider. */ 134 protected CurrentUserProvider _currentUserProvider; 135 /** The handling limited entries helper */ 136 protected LimitedEntriesHelper _handleLimitedEntriesHelper; 137 /** The rights manager */ 138 protected RightManager _rightManager; 139 /** The current user provider */ 140 protected WorkflowProvider _workflowProvider; 141 /** The user manager */ 142 protected UserManager _userManager; 143 /** The system property extension point */ 144 protected FormEntrySystemPropertyExtensionPoint _formSystemPropertyEP; 145 /** The form element definition helper */ 146 protected FormElementDefinitionHelper _formElementDefinitionHelper; 147 /** The form entry DAO */ 148 protected FormEntryDAO _formEntryDAO; 149 /** The searcher factory */ 150 protected SearcherFactory _searcherFactory; 151 /** The form workflow helper */ 152 protected FormWorkflowHelper _formWorkflowHelper; 153 /** The form entries search helper */ 154 protected FormEntriesSearchHelper _formEntriesSearchHelper; 155 156 @Override 157 public void service(ServiceManager serviceManager) throws ServiceException 158 { 159 _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE); 160 _parametersManager = (ParametersManager) serviceManager.lookup(ParametersManager.ROLE); 161 _observationManager = (ObservationManager) serviceManager.lookup(ObservationManager.ROLE); 162 _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE); 163 _handleLimitedEntriesHelper = (LimitedEntriesHelper) serviceManager.lookup(LimitedEntriesHelper.ROLE); 164 _rightManager = (RightManager) serviceManager.lookup(RightManager.ROLE); 165 _workflowProvider = (WorkflowProvider) serviceManager.lookup(WorkflowProvider.ROLE); 166 _userManager = (UserManager) serviceManager.lookup(UserManager.ROLE); 167 _formSystemPropertyEP = (FormEntrySystemPropertyExtensionPoint) serviceManager.lookup(FormEntrySystemPropertyExtensionPoint.ROLE); 168 _formElementDefinitionHelper = (FormElementDefinitionHelper) serviceManager.lookup(FormElementDefinitionHelper.ROLE); 169 _formEntryDAO = (FormEntryDAO) serviceManager.lookup(FormEntryDAO.ROLE); 170 _searcherFactory = (SearcherFactory) serviceManager.lookup(SearcherFactory.ROLE); 171 _formWorkflowHelper = (FormWorkflowHelper) serviceManager.lookup(FormWorkflowHelper.ROLE); 172 _formEntriesSearchHelper = (FormEntriesSearchHelper) serviceManager.lookup(FormEntriesSearchHelper.ROLE); 173 } 174 175 /** 176 * Check if a user have handle data right on a form element as ametys object 177 * @param userIdentity the user 178 * @param formElement the form element 179 * @return true if the user handle data right for a form element 180 */ 181 public boolean hasHandleDataRightOnForm(UserIdentity userIdentity, AmetysObject formElement) 182 { 183 return _rightManager.hasRight(userIdentity, HANDLE_FORMS_ENTRIES_RIGHT_ID, formElement) == RightResult.RIGHT_ALLOW; 184 } 185 186 /** 187 * Check handle data right for a form element as ametys object 188 * @param formElement the form element as ametys object 189 */ 190 public void checkHandleDataRight(AmetysObject formElement) 191 { 192 UserIdentity user = _currentUserProvider.getUser(); 193 if (user == null) 194 { 195 // User not yet authenticated 196 throw new AuthorizationRequiredException(); 197 } 198 199 if (!hasHandleDataRightOnForm(user, formElement)) 200 { 201 throw new AccessDeniedException("User '" + user + "' tried to handle form data without convenient right [" + HANDLE_FORMS_ENTRIES_RIGHT_ID + "]"); 202 } 203 } 204 205 /** 206 * Gets properties of a form entry 207 * @param id The id of the form entry 208 * @return The properties 209 */ 210 @Callable (rights = Callable.NO_CHECK_REQUIRED) 211 public Map<String, Object> getFormEntryProperties (String id) 212 { 213 // Assume that no read access is checked (required for bus message target) 214 try 215 { 216 FormEntry entry = _resolver.resolveById(id); 217 return getFormEntryProperties(entry); 218 } 219 catch (UnknownAmetysObjectException e) 220 { 221 getLogger().warn("Can't find entry with id: {}. It probably has just been deleted", id, e); 222 Map<String, Object> infos = new HashMap<>(); 223 infos.put("id", id); 224 return infos; 225 } 226 } 227 228 /** 229 * Gets properties of a form entry 230 * @param entry The form entry 231 * @return The properties 232 */ 233 public Map<String, Object> getFormEntryProperties (FormEntry entry) 234 { 235 Map<String, Object> properties = new HashMap<>(); 236 237 properties.put("id", entry.getId()); 238 properties.put("formId", entry.getForm().getId()); 239 properties.put("rights", _getUserRights(entry)); 240 241 return properties; 242 } 243 244 /** 245 * Get user rights for the given form entry 246 * @param entry the form entry 247 * @return the set of rights 248 */ 249 protected Set<String> _getUserRights (FormEntry entry) 250 { 251 UserIdentity user = _currentUserProvider.getUser(); 252 return _rightManager.getUserRights(user, entry); 253 } 254 255 /** 256 * Creates a {@link FormEntry}. 257 * @param form The parent form 258 * @param clientIP The client IP address 259 * @return return the form entry 260 */ 261 public FormEntry createEntry(Form form, String clientIP) 262 { 263 ModifiableTraversableAmetysObject entriesRoot; 264 if (form.hasChild(ENTRIES_ROOT)) 265 { 266 entriesRoot = form.getChild(ENTRIES_ROOT); 267 } 268 else 269 { 270 entriesRoot = form.createChild(ENTRIES_ROOT, "ametys:collection"); 271 } 272 // Find unique name 273 String originalName = "entry"; 274 String uniqueName = originalName + "-1"; 275 int index = 2; 276 while (entriesRoot.hasChild(uniqueName)) 277 { 278 uniqueName = originalName + "-" + (index++); 279 } 280 FormEntry entry = (FormEntry) entriesRoot.createChild(uniqueName, "ametys:form-entry"); 281 282 UserIdentity user = _currentUserProvider.getUser(); 283 entry.setUser(user); 284 entry.setIP(clientIP); 285 entry.setSubmitDate(ZonedDateTime.now()); 286 entry.setActive(true); 287 _setEntryId(entry); 288 289 entry.saveChanges(); 290 291 Map<String, Object> formParams = new HashMap<>(); 292 formParams.put("form", form); 293 _observationManager.notify(new Event(ObservationConstants.FORM_MODIFIED, user, formParams)); 294 295 Map<String, Object> eventParams = new HashMap<>(); 296 eventParams.put(ObservationConstants.ARGS_FORM_ENTRY_ID, entry.getId()); 297 _observationManager.notify(new Event(ObservationConstants.FORM_ENTRY_ADDED, user, eventParams)); 298 299 return entry; 300 } 301 302 private void _setEntryId(FormEntry entry) 303 { 304 List<FormEntry> formEntries = getFormEntries(entry.getForm(), false, List.of(new Sort(FormEntry.ATTRIBUTE_ID, "descending"))); 305 Long entryId = formEntries.isEmpty() ? 1L : formEntries.get(0).getEntryId() + 1; 306 entry.setEntryId(entryId); 307 } 308 309 /** 310 * Edit a form entry 311 * @param entry the form entry 312 * @param values the values to set 313 * @param editView the edit view 314 * @param isJustCreated <code>true</code> if the entry is just created before edition 315 * @throws Exception if an error occurs 316 */ 317 public void editFormEntry(FormEntry entry, Map<String, Object> values, View editView, boolean isJustCreated) throws Exception 318 { 319 Form form = entry.getForm(); 320 321 entry.synchronizeValues(editView, values); 322 _handleComputedValues(form.getQuestions(), entry, !isJustCreated); 323 324 if (isJustCreated) 325 { 326 // For creation, workflow needs to be initialized after setting values for send mail function can have access to all entry data 327 _formWorkflowHelper.initializeWorkflow(entry); 328 } 329 330 form.saveChanges(); 331 332 Map<String, Object> eventParams = new HashMap<>(); 333 eventParams.put(ObservationConstants.ARGS_FORM_ENTRY_ID, entry.getId()); 334 _observationManager.notify(new Event(ObservationConstants.FORM_ENTRY_MODIFIED, _currentUserProvider.getUser(), eventParams)); 335 } 336 337 /** 338 * Handle computed values 339 * @param questions the form questions 340 * @param entry the entry 341 * @param forEdition <code>true</code> to handle edition 342 */ 343 protected void _handleComputedValues(List<FormQuestion> questions, FormEntry entry, boolean forEdition) 344 { 345 for (FormQuestion question : questions) 346 { 347 FormQuestionType questionType = question.getType(); 348 if (questionType instanceof ComputedQuestionType type) 349 { 350 if (!forEdition || type.getComputingType(question).canEdit()) 351 { 352 Object computedValue = type.getComputingType(question).getComputedValue(question, entry); 353 if (computedValue != null) 354 { 355 entry.setValue(question.getNameForForm(), computedValue); 356 } 357 } 358 } 359 } 360 } 361 362 /** 363 * Get the search model configuration to search form entries 364 * @param formId the identifier of form 365 * @return The search model configuration 366 * @throws ProcessingException If an error occurred 367 */ 368 @Callable (rights = HANDLE_FORMS_ENTRIES_RIGHT_ID, rightContext = FormsDirectoryRightAssignmentContext.ID, paramIndex = 0) 369 public Map<String, Object> getSearchModelConfiguration (String formId) throws ProcessingException 370 { 371 Map<String, Object> result = new HashMap<>(); 372 373 Form form = _resolver.resolveById(formId); 374 result.put("criteria", _getCriteria(form)); 375 result.put("columns", _getColumns(form)); 376 377 result.put("searchUrlPlugin", "forms"); 378 result.put("searchUrl", "form/entries.json"); 379 result.put("pageSize", 50); 380 return result; 381 } 382 383 /** 384 * Get criteria to search form entries 385 * @param form the form 386 * @return the criteria as JSON 387 */ 388 protected Map<String, Object> _getCriteria(Form form) 389 { 390 // Currently, return no criteria for search entries tool 391 return Map.of(); 392 } 393 394 /** 395 * Get the columns for search form entries 396 * @param form the form 397 * @return the columns as JSON 398 * @throws ProcessingException if an error occurred 399 */ 400 protected List<Map<String, Object>> _getColumns(Form form) throws ProcessingException 401 { 402 List<SearchUIColumn> columns = new ArrayList<>(); 403 404 Model formEntryModel = getFormEntryModel(form); 405 406 ModelItem idAttribute = formEntryModel.getModelItem(FormEntry.ATTRIBUTE_ID); 407 SearchUIColumn idColumn = SearchUIColumnHelper.createModelItemColumn(idAttribute); 408 idColumn.setWidth(80); 409 columns.add(idColumn); 410 411 ModelItem userAttribute = formEntryModel.getModelItem(FormEntry.ATTRIBUTE_USER); 412 SearchUIColumn userColumn = SearchUIColumnHelper.createModelItemColumn(userAttribute); 413 userColumn.setWidth(150); 414 columns.add(userColumn); 415 416 ModelItem submitDateAttribute = formEntryModel.getModelItem(FormEntry.ATTRIBUTE_SUBMIT_DATE); 417 SearchUIColumn submitDateColumn = SearchUIColumnHelper.createModelItemColumn(submitDateAttribute); 418 submitDateColumn.setWidth(150); 419 columns.add(submitDateColumn); 420 421 for (FormEntryColumn column : _formEntriesSearchHelper.getFormEntryColumns(form).values()) 422 { 423 ModelItem modelItem = column.modelItem(); 424 String modelItemName = modelItem.getName(); 425 // Do not add system attributes to the columns list, they are already added above 426 if (!modelItemName.equals(FormEntry.ATTRIBUTE_ID) 427 && !modelItemName.equals(FormEntry.ATTRIBUTE_USER) 428 && !modelItemName.equals(FormEntry.ATTRIBUTE_SUBMIT_DATE)) 429 { 430 SearchUIColumn<? extends ModelItem> uiColumn = SearchUIColumnHelper.createModelItemColumn(modelItem); 431 432 FormQuestion question = form.getQuestion(modelItemName); 433 if (question != null) 434 { 435 FormQuestionType type = question.getType(); 436 437 uiColumn.setRenderer(Optional.ofNullable(type.getJSRenderer(question)) 438 .filter(StringUtils::isNotBlank)); 439 440 uiColumn.setConverter(Optional.ofNullable(type.getJSConverter(question)) 441 .filter(StringUtils::isNotBlank)); 442 443 uiColumn.setSortable(column.sortFieldName() != null); 444 } 445 446 columns.add(uiColumn); 447 } 448 } 449 450 List<Map<String, Object>> columnsInfo = new ArrayList<>(); 451 DefinitionContext definitionContext = DefinitionContext.newInstance(); 452 for (SearchUIColumn column : columns) 453 { 454 columnsInfo.add(column.toJSON(definitionContext)); 455 } 456 457 if (form.isQueueEnabled()) 458 { 459 columnsInfo.add( 460 Map.of("name", FormEntry.SYSTEM_ATTRIBUTE_PREFIX + GetFormEntriesAction.QUEUE_STATUS, 461 "label", new I18nizableText("plugin.forms", "PLUGINS_FORMS_QUEUE_STATUS_COLUMN_TITLE_LABEL"), 462 "type", ModelItemTypeConstants.BOOLEAN_TYPE_ID, 463 "path", FormEntry.SYSTEM_ATTRIBUTE_PREFIX + GetFormEntriesAction.QUEUE_STATUS 464 ) 465 ); 466 } 467 468 columnsInfo.add( 469 Map.of("name", FormEntry.SYSTEM_ATTRIBUTE_PREFIX + GetFormEntriesAction.FORM_ENTRY_ACTIVE, 470 "label", new I18nizableText("plugin.forms", "PLUGINS_FORMS_ENTRY_ACTIVE_COLUMN_TITLE_LABEL"), 471 "type", ModelItemTypeConstants.BOOLEAN_TYPE_ID, 472 "path", FormEntry.SYSTEM_ATTRIBUTE_PREFIX + GetFormEntriesAction.FORM_ENTRY_ACTIVE, 473 "hidden", true 474 ) 475 ); 476 477 return columnsInfo; 478 } 479 480 /** 481 * Deletes a {@link FormEntry}. 482 * @param id The id of the form entry to delete 483 * @return The entry data 484 */ 485 @Callable (rights = DELETE_FORMS_ENTRIES_RIGHT_ID, rightContext = FormsDirectoryRightAssignmentContext.ID, paramIndex = 0) 486 public Map<String, String> deleteEntry (String id) 487 { 488 Map<String, String> result = new HashMap<>(); 489 490 FormEntry entry = _resolver.resolveById(id); 491 492 _handleLimitedEntriesHelper.deactivateEntry(id); 493 494 Form form = entry.getForm(); 495 entry.remove(); 496 497 _observationManager.notify(new Event(ObservationConstants.FORM_ENTRY_DELETED, _currentUserProvider.getUser(), Map.of(ObservationConstants.ARGS_FORM_ENTRY_IDS, List.of(id)))); 498 499 form.saveChanges(); 500 501 Map<String, Object> eventParams = new HashMap<>(); 502 eventParams.put("form", form); 503 _observationManager.notify(new Event(ObservationConstants.FORM_MODIFIED, _currentUserProvider.getUser(), eventParams)); 504 505 result.put("entryId", id); 506 result.put("formId", form.getId()); 507 result.put("hasEntries", String.valueOf(form.hasEntries())); 508 509 return result; 510 } 511 512 /** 513 * Delete all entries of a form 514 * @param id The id of the form 515 * @return the deleted entries data 516 */ 517 @Callable (rights = DELETE_FORMS_ENTRIES_RIGHT_ID, rightContext = FormsDirectoryRightAssignmentContext.ID, paramIndex = 0) 518 public Map<String, Object> clearEntries(String id) 519 { 520 Map<String, Object> result = new HashMap<>(); 521 List<String> entryIds = new ArrayList<>(); 522 Form form = _resolver.resolveById(id); 523 524 for (FormEntry entry: form.getEntries()) 525 { 526 entryIds.add(entry.getId()); 527 entry.remove(); 528 } 529 530 _observationManager.notify(new Event(ObservationConstants.FORM_ENTRY_DELETED, _currentUserProvider.getUser(), Map.of(ObservationConstants.ARGS_FORM_ENTRY_IDS, entryIds))); 531 532 form.saveChanges(); 533 Map<String, Object> eventParams = new HashMap<>(); 534 eventParams.put("form", form); 535 _observationManager.notify(new Event(ObservationConstants.FORM_MODIFIED, _currentUserProvider.getUser(), eventParams)); 536 537 result.put("ids", entryIds); 538 result.put("formId", form.getId()); 539 540 return result; 541 } 542 543 /** 544 * Retrieves the current step id of the form entry 545 * @param entry The form entry 546 * @return the current step id 547 * @throws AmetysRepositoryException if an error occurs. 548 */ 549 public Long getCurrentStepId(FormEntry entry) throws AmetysRepositoryException 550 { 551 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(entry); 552 try 553 { 554 Step currentStep = (Step) workflow.getCurrentSteps(entry.getWorkflowId()).iterator().next(); 555 return Long.valueOf(currentStep.getStepId()); 556 } 557 catch (AmetysRepositoryException e) 558 { 559 return RestrictiveAwareQuestionType.INITIAL_WORKFLOW_ID; // can occur when entry has just been created and workflow is not yet initialized 560 } 561 } 562 563 /** 564 * Get the form entry model 565 * @param form the form 566 * @return the form entry model 567 */ 568 public Model getFormEntryModel(Form form) 569 { 570 List<ModelItem> items = new ArrayList<>(); 571 for (FormQuestion question : form.getQuestions()) 572 { 573 FormQuestionType type = question.getType(); 574 if (!type.onlyForDisplay(question)) 575 { 576 Model entryModel = question.getType().getEntryModel(question); 577 for (ModelItem modelItem : entryModel.getModelItems()) 578 { 579 items.add(modelItem); 580 } 581 582 if (type instanceof ChoicesListQuestionType cLType) 583 { 584 ModelItem otherFieldModel = cLType.getOtherFieldModel(question); 585 if (otherFieldModel != null) 586 { 587 items.add(otherFieldModel); 588 } 589 } 590 } 591 } 592 593 items.add(_formElementDefinitionHelper.getElementDefinition(FormEntry.ATTRIBUTE_ID, ModelItemTypeConstants.LONG_TYPE_ID, "PLUGIN_FORMS_MODEL_ITEM_ID_LABEL", null, null)); 594 items.add(_formElementDefinitionHelper.getElementDefinition(FormEntry.ATTRIBUTE_USER, ModelItemTypeConstants.USER_ELEMENT_TYPE_ID, "PLUGIN_FORMS_MODEL_ITEM_USER_LABEL", null, null)); 595 items.add(_formElementDefinitionHelper.getElementDefinition(FormEntry.ATTRIBUTE_ACTIVE, ModelItemTypeConstants.BOOLEAN_TYPE_ID, "PLUGIN_FORMS_MODEL_ITEM_ACTIVE_LABEL", null, null)); 596 items.add(_formElementDefinitionHelper.getElementDefinition(FormEntry.ATTRIBUTE_SUBMIT_DATE, ModelItemTypeConstants.DATETIME_TYPE_ID, "PLUGIN_FORMS_MODEL_ITEM_SUBMISSION_DATE_LABEL", null, null)); 597 items.add(_formElementDefinitionHelper.getElementDefinition(FormEntry.ATTRIBUTE_IP, ModelItemTypeConstants.STRING_TYPE_ID, "PLUGIN_FORMS_MODEL_ITEM_IP_LABEL", null, null)); 598 items.add(_formElementDefinitionHelper.getElementDefinition(FormEntry.ATTRIBUTE_ANONYMIZATION_DATE, ModelItemTypeConstants.DATETIME_TYPE_ID, "PLUGIN_FORMS_MODEL_ITEM_ANONYMIZATION_DATE_LABEL", null, null)); 599 600 for (String id : _formSystemPropertyEP.getExtensionsIds()) 601 { 602 items.add(_formSystemPropertyEP.getExtension(id)); 603 } 604 605 return Model.of( 606 "form.entry.model.id", 607 "form.entry.model.family.id", 608 items.toArray(ModelItem[]::new) 609 ); 610 } 611 612 /** 613 * Get all form entries 614 * @param site the site. Can be null to get all form entries of all sites 615 * @return the list of all form entries 616 */ 617 public List<FormEntry> getAllFormEntries(Site site) 618 { 619 String formEntryQuery = QueryHelper.getXPathQuery(null, "ametys:form-entry", null, null); 620 String xPathQuery = site != null 621 ? QueryHelper.getXPathQuery(site.getName(), "ametys:site", null, null) + formEntryQuery 622 : formEntryQuery; 623 AmetysObjectIterable<FormEntry> formEntries = _resolver.query(xPathQuery); 624 return formEntries.stream().collect(Collectors.toList()); 625 } 626 627 /** 628 * Get the form entries 629 * @param form the form 630 * @param onlyActiveEntries <code>true</code> to have only active entries 631 * @param sorts the list of sort 632 * @return the form entries 633 */ 634 public List<FormEntry> getFormEntries(Form form, boolean onlyActiveEntries, List<Sort> sorts) 635 { 636 return getFormEntries(form, onlyActiveEntries, null, sorts); 637 } 638 639 /** 640 * Get the form entries 641 * @param form the form 642 * @param onlyActiveEntries <code>true</code> to have only active entries 643 * @param additionalEntryFilterExpr the additional entry filter expression. Can be null. 644 * @param sorts the list of sort 645 * @return the form entries 646 */ 647 public List<FormEntry> getFormEntries(Form form, boolean onlyActiveEntries, Expression additionalEntryFilterExpr, List<Sort> sorts) 648 { 649 try 650 { 651 String uuid = form.getNode().getIdentifier(); 652 String xpathQuery = "//element(*, ametys:form)[@" + JcrConstants.JCR_UUID + " = '" + uuid + "']//element(*, ametys:form-entry)"; 653 654 String entryFilterQuery = _getEntryFilterQuery(onlyActiveEntries, additionalEntryFilterExpr); 655 if (onlyActiveEntries || StringUtils.isNotBlank(entryFilterQuery)) 656 { 657 xpathQuery += "[" + entryFilterQuery + "]"; 658 } 659 660 String sortsAsString = ""; 661 for (Sort sort : sorts) 662 { 663 if (StringUtils.isNotBlank(sortsAsString)) 664 { 665 sortsAsString += ", "; 666 } 667 668 sortsAsString += "@ametys:" + sort.attributeName() + " " + sort.direction(); 669 } 670 671 if (StringUtils.isNotBlank(sortsAsString)) 672 { 673 xpathQuery += " order by " + sortsAsString; 674 } 675 676 AmetysObjectIterable<FormEntry> formEntries = _resolver.query(xpathQuery); 677 return formEntries.stream().collect(Collectors.toList()); 678 } 679 catch (RepositoryException e) 680 { 681 getLogger().error("An error occurred getting entries of form '" + form.getId() + "'"); 682 } 683 684 return List.of(); 685 } 686 687 private String _getEntryFilterQuery(boolean onlyActiveEntries, Expression additionalEntryFilterExpr) 688 { 689 List<Expression> expressions = new ArrayList<>(); 690 if (onlyActiveEntries) 691 { 692 expressions.add(new BooleanExpression(FormEntry.ATTRIBUTE_ACTIVE, true)); 693 } 694 695 if (additionalEntryFilterExpr != null) 696 { 697 expressions.add(additionalEntryFilterExpr); 698 } 699 700 return new AndExpression(expressions).build(); 701 } 702 703 /** 704 * Search the form entries 705 * @param siteName the site name 706 * @param form the form 707 * @param onlyActiveEntries <code>true</code> to have only active entries. Active entries are used in waiting list. 708 * @param actionableOnly <code>true</code> to return only entry with at least one available action for the current user 709 * @param checkRights <code>true</code> to check rights for returned entries 710 * @param additionalEntryFilterQuery the additional entry filter query. Can be null. 711 * @param facets the list of facet 712 * @param sorts the list of sort 713 * @param offset the offset of the first result to return 714 * @param length the maximum number of results to return 715 * @return the form entries 716 * @throws Exception if an error occurred during search 717 */ 718 public SearchResults<FormEntry> searchFormEntries(String siteName, Form form, boolean onlyActiveEntries, boolean actionableOnly, boolean checkRights, Query additionalEntryFilterQuery, List<FacetDefinition> facets, List<SortDefinition> sorts, int offset, int length) throws Exception 719 { 720 List<Query> queries = new ArrayList<>(); 721 if (form != null) 722 { 723 queries.add(new FormQuery(form.getId())); 724 } 725 726 if (onlyActiveEntries) 727 { 728 queries.add(new BooleanQuery(FormEntry.ATTRIBUTE_ACTIVE, true)); 729 } 730 731 if (actionableOnly) 732 { 733 UserIdentity user = _currentUserProvider.getUser(); 734 AndQuery actorsQuery = new AndQuery( 735 new UsersQuery(FormEntry.SYSTEM_PROPERTY_WORKFLOW_DENIED_ACTORS + "_s", Operator.NE, user), // current user must not be in denied actors 736 new OrQuery( 737 new UsersQuery(FormEntry.SYSTEM_PROPERTY_WORKFLOW_ALLOWED_ACTORS + "_s", user), // current user mut be in allowed actors ... 738 new BooleanQuery(FormEntry.SYSTEM_PROPERTY_ANY_CONNECTED_WORKFLOW_ACTORS, true) // ... or in any connected workflow actors 739 ) 740 ); 741 queries.add(actorsQuery); 742 } 743 744 if (additionalEntryFilterQuery != null) 745 { 746 queries.add(additionalEntryFilterQuery); 747 } 748 749 queries.add(new NotQuery(new DateQuery(FormEntry.ATTRIBUTE_ANONYMIZATION_DATE))); 750 queries.add(new SiteQuery(siteName)); 751 752 Searcher searcher = _searcherFactory.create() 753 .withQuery(new AndQuery(queries)) 754 .withFacets(facets) 755 .addFilterQuery(new DocumentTypeQuery(SolrFormEntryIndexer.TYPE_FORM_ENTRY)) 756 .withSort(sorts) 757 .withLimits(offset, length) 758 .setCheckRights(checkRights); 759 760 return searcher.searchWithFacets(); 761 } 762 763 764 /** 765 * Get all users who answer to the form as JSON 766 * @param formId the form id 767 * @return all users as JSON 768 */ 769 @Callable (rights = HANDLE_FORMS_ENTRIES_RIGHT_ID, paramIndex = 0, rightContext = FormsDirectoryRightAssignmentContext.ID, decorators = ForceDefaultRepositoryWorkspaceCallableDecorator.DECORATOR_ID) 770 public List<Map<String, String>> getFormEntriesUsers(String formId) 771 { 772 Form form = _resolver.resolveById(formId); 773 List<FacetDefinition> facets = List.of(new FacetDefinition(FormEntry.ATTRIBUTE_USER, FormEntry.ATTRIBUTE_USER + "_s")); 774 775 try 776 { 777 SearchResults<FormEntry> searchFormEntries = searchFormEntries( 778 form.getSiteName(), 779 form, 780 false, // do not filter on active entries 781 true, // only entries with at least one available action for the current user 782 false, // do not check read rights 783 null, 784 facets, 785 List.of(), 786 0, 787 Integer.MAX_VALUE 788 ); 789 790 return searchFormEntries.getFacetResults().get(FormEntry.ATTRIBUTE_USER) 791 .keySet() 792 .stream() 793 .map(id -> UserIdentity.stringToUserIdentity(id)) 794 .map(_userManager::getUser) 795 .filter(Objects::nonNull) 796 .sorted(Comparator.comparing(User::getFullName, String.CASE_INSENSITIVE_ORDER)) 797 .map(u -> Map.of("text", u.getFullName(), "id", UserIdentity.userIdentityToString(u.getIdentity()))) 798 .toList(); 799 } 800 catch (Exception e) 801 { 802 getLogger().error("An error occurred while searching users who answer to form '" + formId + "'", e); 803 return List.of(); 804 } 805 } 806 807 /** 808 * Record representing a sort with form attribute name and direction 809 * @param attributeName the attribute name 810 * @param direction the direction 811 */ 812 public record Sort(String attributeName, String direction) { /* */ } 813}