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