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