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.Collection; 020import java.util.HashMap; 021import java.util.List; 022import java.util.Map; 023 024import org.apache.avalon.framework.component.Component; 025import org.apache.avalon.framework.service.ServiceException; 026import org.apache.avalon.framework.service.ServiceManager; 027import org.apache.avalon.framework.service.Serviceable; 028import org.apache.cocoon.ProcessingException; 029import org.apache.commons.lang3.StringUtils; 030 031import org.ametys.core.observation.Event; 032import org.ametys.core.observation.ObservationManager; 033import org.ametys.core.right.RightManager; 034import org.ametys.core.right.RightManager.RightResult; 035import org.ametys.core.ui.Callable; 036import org.ametys.core.user.CurrentUserProvider; 037import org.ametys.plugins.forms.FormEvents; 038import org.ametys.plugins.forms.actions.GetFormEntriesAction; 039import org.ametys.plugins.forms.helper.LimitedEntriesHelper; 040import org.ametys.plugins.forms.question.FormQuestionType; 041import org.ametys.plugins.forms.question.types.ChoicesListQuestionType; 042import org.ametys.plugins.forms.question.types.FileQuestionType; 043import org.ametys.plugins.forms.question.types.MatrixQuestionType; 044import org.ametys.plugins.forms.question.types.RichTextQuestionType; 045import org.ametys.plugins.forms.repository.Form; 046import org.ametys.plugins.forms.repository.FormEntry; 047import org.ametys.plugins.forms.repository.FormQuestion; 048import org.ametys.plugins.repository.AmetysObjectResolver; 049import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 050import org.ametys.plugins.repository.UnknownAmetysObjectException; 051import org.ametys.runtime.i18n.I18nizableText; 052import org.ametys.runtime.model.DefinitionContext; 053import org.ametys.runtime.model.Model; 054import org.ametys.runtime.model.View; 055import org.ametys.runtime.model.type.ModelItemTypeConstants; 056import org.ametys.runtime.plugin.component.AbstractLogEnabled; 057import org.ametys.web.parameters.ParametersManager; 058 059/** 060 * Form entry DAO. 061 */ 062public class FormEntryDAO extends AbstractLogEnabled implements Serviceable, Component 063{ 064 /** The Avalon role name. */ 065 public static final String ROLE = FormEntryDAO.class.getName(); 066 067 /** Name for entries root jcr node */ 068 public static final String ENTRIES_ROOT = "ametys-internal:form-entries"; 069 070 /** The right id to consult form entries */ 071 public static final String HANDLE_FORMS_ENTRIES_RIGHT_ID = "Form_Entries_Rights_Data"; 072 073 /** The right id to delete form entries */ 074 public static final String DELETE_FORMS_ENTRIES_RIGHT_ID = "Runtime_Rights_Forms_Entry_Delete"; 075 076 /** Ametys object resolver. */ 077 protected AmetysObjectResolver _resolver; 078 /** The parameters manager */ 079 protected ParametersManager _parametersManager; 080 /** Observer manager. */ 081 protected ObservationManager _observationManager; 082 /** The current user provider. */ 083 protected CurrentUserProvider _currentUserProvider; 084 /** The handling limited entries helper */ 085 protected LimitedEntriesHelper _handleLimitedEntriesHelper; 086 /** The rights manager */ 087 protected RightManager _rightManager; 088 089 @Override 090 public void service(ServiceManager serviceManager) throws ServiceException 091 { 092 _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE); 093 _parametersManager = (ParametersManager) serviceManager.lookup(ParametersManager.ROLE); 094 _observationManager = (ObservationManager) serviceManager.lookup(ObservationManager.ROLE); 095 _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE); 096 _handleLimitedEntriesHelper = (LimitedEntriesHelper) serviceManager.lookup(LimitedEntriesHelper.ROLE); 097 _rightManager = (RightManager) serviceManager.lookup(RightManager.ROLE); 098 } 099 100 /** 101 * Gets properties of a form entry 102 * @param id The id of the form entry 103 * @return The properties 104 */ 105 @Callable 106 public Map<String, Object> getFormEntryProperties (String id) 107 { 108 try 109 { 110 FormEntry entry = _resolver.resolveById(id); 111 return getFormEntryProperties(entry); 112 } 113 catch (UnknownAmetysObjectException e) 114 { 115 getLogger().warn("Can't find entry with id: {}. It probably has just been deleted", id, e); 116 Map<String, Object> infos = new HashMap<>(); 117 infos.put("id", id); 118 return infos; 119 } 120 } 121 122 /** 123 * Gets properties of a form entry 124 * @param entry The form entry 125 * @return The properties 126 */ 127 public Map<String, Object> getFormEntryProperties (FormEntry entry) 128 { 129 Map<String, Object> properties = new HashMap<>(); 130 131 properties.put("id", entry.getId()); 132 properties.put("formId", entry.getForm().getId()); 133 134 try 135 { 136 properties.put("path", entry.getNode().getPath()); 137 properties.put("uuid", entry.getNode().getIdentifier()); 138 } 139 catch (Exception e) 140 { 141 getLogger().error("Can't have JCR property for form entry id '{}'.", entry.getId(), e); 142 } 143 144 return properties; 145 } 146 147 /** 148 * Creates a {@link FormEntry}. 149 * @param form The parent form 150 * @return return the form entry 151 */ 152 public FormEntry createEntry(Form form) 153 { 154 ModifiableTraversableAmetysObject entriesRoot; 155 if (form.hasChild(ENTRIES_ROOT)) 156 { 157 entriesRoot = form.getChild(ENTRIES_ROOT); 158 } 159 else 160 { 161 entriesRoot = form.createChild(ENTRIES_ROOT, "ametys:collection"); 162 } 163 // Find unique name 164 String originalName = "entry"; 165 String uniqueName = originalName + "-1"; 166 int index = 2; 167 while (entriesRoot.hasChild(uniqueName)) 168 { 169 uniqueName = originalName + "-" + (index++); 170 } 171 FormEntry entry = (FormEntry) entriesRoot.createChild(uniqueName, "ametys:form-entry"); 172 173 Map<String, Object> eventParams = new HashMap<>(); 174 eventParams.put("form", form); 175 _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _currentUserProvider.getUser(), eventParams)); 176 177 return entry; 178 } 179 180 /** 181 * Get the columns information of a form 182 * @param formId the identifier of form 183 * @return The columns 184 * @throws IllegalArgumentException If an error occurred 185 * @throws ProcessingException If an error occurred 186 */ 187 @Callable 188 public Map<String, Object> getColumns (String formId) throws ProcessingException, IllegalArgumentException 189 { 190 Map<String, Object> result = new HashMap<>(); 191 192 Form form = _resolver.resolveById(formId); 193 List<FormEntry> entries = form.getEntries(); 194 if (entries.isEmpty()) 195 { 196 throw new IllegalAccessError("Can't call getColumns method because the form with id '" + formId + "' has no entries"); 197 } 198 199 FormEntry entry = form.getEntries().get(0); //Get first form entry just for the model 200 Collection< ? extends Model> model = entry.getModel(); 201 Map<String, Object> json = View.of(model).toJSON(DefinitionContext.newInstance()); 202 result.put("criteria", json); 203 204 List<Map<String, Object>> columns = new ArrayList<>(); 205 columns.add( 206 Map.of("name", FormEntry.ATTRIBUTE_ID, 207 "label", new I18nizableText("plugin.forms", "PLUGIN_FORMS_MODEL_ITEM_ID_LABEL"), 208 "type", ModelItemTypeConstants.LONG_TYPE_ID, 209 "id", form.getId() + FormEntry.ATTRIBUTE_ID, 210 "width", 80 211 ) 212 ); 213 214 columns.add( 215 Map.of("name", FormEntry.ATTRIBUTE_USER, 216 "label", new I18nizableText("plugin.forms", "PLUGIN_FORMS_MODEL_ITEM_USER_LABEL"), 217 "type", org.ametys.cms.data.type.ModelItemTypeConstants.USER_ELEMENT_TYPE_ID, 218 "id", form.getId() + FormEntry.ATTRIBUTE_USER, 219 "width", 150 220 ) 221 ); 222 223 columns.add( 224 Map.of("name", FormEntry.ATTRIBUTE_SUBMIT_DATE, 225 "label", new I18nizableText("plugin.forms", "PLUGIN_FORMS_MODEL_ITEM_SUBMISSION_DATE_LABEL"), 226 "type", ModelItemTypeConstants.DATETIME_TYPE_ID, 227 "id", form.getId() + FormEntry.ATTRIBUTE_SUBMIT_DATE, 228 "width", 150 229 ) 230 ); 231 232 @SuppressWarnings("unchecked") 233 Map<String, Object> elements = (Map<String, Object>) json.get("elements"); 234 for (String id : elements.keySet()) 235 { 236 if (!id.equals(FormEntry.ATTRIBUTE_IP) 237 && !id.equals(FormEntry.ATTRIBUTE_ACTIVE) 238 && !id.equals(FormEntry.ATTRIBUTE_SUBMIT_DATE) 239 && !id.equals(FormEntry.ATTRIBUTE_ID) 240 && !id.equals(FormEntry.ATTRIBUTE_USER) 241 && !id.startsWith(ChoicesListQuestionType.OTHER_PREFIX_DATA_NAME)) 242 { 243 @SuppressWarnings("unchecked") 244 Map<String, Object> column = (Map<String, Object>) elements.get(id); 245 column.put("id", form.getId() + id); 246 247 FormQuestion question = form.getQuestion(id); 248 if (question != null) 249 { 250 FormQuestionType type = question.getType(); 251 String jsRenderer = type.getJSRenderer(question); 252 if (StringUtils.isNotBlank(jsRenderer)) 253 { 254 column.put("renderer", jsRenderer); 255 } 256 257 String jsConverter = type.getJSConverter(question); 258 if (StringUtils.isNotBlank(jsConverter)) 259 { 260 column.put("converter", jsConverter); 261 } 262 } 263 264 if (question != null && !_isSortable(entry, question)) 265 { 266 column.put("sortable", false); 267 } 268 269 columns.add(column); 270 } 271 } 272 273 if (form.isQueueEnabled()) 274 { 275 columns.add( 276 Map.of("name", GetFormEntriesAction.QUEUE_STATUS, 277 "label", new I18nizableText("plugin.forms", "PLUGINS_FORMS_QUEUE_STATUS_COLUMN_TITLE_LABEL"), 278 "type", ModelItemTypeConstants.BOOLEAN_TYPE_ID, 279 "id", GetFormEntriesAction.QUEUE_STATUS 280 ) 281 ); 282 } 283 284 if (form.hasWorkflow()) 285 { 286 columns.add( 287 Map.of("name", GetFormEntriesAction.FORM_ENTRY_STATUS_ID, 288 "label", new I18nizableText("plugin.forms", "PLUGINS_FORMS_WORKFLOW_TAB_STATUS_COLUMN_TITLE_LABEL"), 289 "type", ModelItemTypeConstants.STRING_TYPE_ID, 290 "id", GetFormEntriesAction.FORM_ENTRY_STATUS_ID 291 ) 292 ); 293 } 294 295 columns.add( 296 Map.of("name", GetFormEntriesAction.FORM_ENTRY_ACTIVE, 297 "label", new I18nizableText("plugin.forms", "PLUGINS_FORMS_ENTRY_ACTIVE_COLUMN_TITLE_LABEL"), 298 "type", ModelItemTypeConstants.BOOLEAN_TYPE_ID, 299 "id", GetFormEntriesAction.FORM_ENTRY_ACTIVE, 300 "hidden", true 301 ) 302 ); 303 304 result.put("columns", columns); 305 306 result.put("searchUrlPlugin", "forms"); 307 result.put("searchUrl", "form/entries.json"); 308 result.put("pageSize", 50); 309 return result; 310 } 311 312 /** 313 * <code>true</code> if the column link to the question is sortable 314 * @param entry the entry for the model 315 * @param question the question 316 * @return <code>true</code> if the column link to the question is sortable 317 */ 318 protected boolean _isSortable(FormEntry entry, FormQuestion question) 319 { 320 if (entry.isMultiple(question.getNameForForm())) 321 { 322 return false; 323 } 324 325 FormQuestionType type = question.getType(); 326 if (type instanceof MatrixQuestionType || type instanceof RichTextQuestionType || type instanceof FileQuestionType) 327 { 328 return false; 329 } 330 331 return true; 332 } 333 334 /** 335 * Deletes a {@link FormEntry}. 336 * @param id The id of the form entry to delete 337 * @return The entry data 338 */ 339 @Callable 340 public Map<String, String> deleteEntry (String id) 341 { 342 Map<String, String> result = new HashMap<>(); 343 344 FormEntry entry = _resolver.resolveById(id); 345 346 if (_rightManager.currentUserHasRight(DELETE_FORMS_ENTRIES_RIGHT_ID, entry) != RightResult.RIGHT_ALLOW) 347 { 348 result.put("message", "not-allowed"); 349 return result; 350 } 351 352 _handleLimitedEntriesHelper.deactivateEntry(id); 353 354 Form form = entry.getForm(); 355 entry.remove(); 356 357 form.saveChanges(); 358 359 Map<String, Object> eventParams = new HashMap<>(); 360 eventParams.put("form", form); 361 _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _currentUserProvider.getUser(), eventParams)); 362 363 result.put("entryId", id); 364 result.put("formId", form.getId()); 365 result.put("hasEntries", String.valueOf(form.hasEntries())); 366 367 return result; 368 } 369 370 /** 371 * Delete all entries of a form 372 * @param id The id of the form 373 * @return the deleted entries data 374 */ 375 @Callable 376 public Map<String, Object> clearEntries(String id) 377 { 378 Map<String, Object> result = new HashMap<>(); 379 List<String> entryIds = new ArrayList<>(); 380 Form form = _resolver.resolveById(id); 381 if (_rightManager.currentUserHasRight(DELETE_FORMS_ENTRIES_RIGHT_ID, form) != RightResult.RIGHT_ALLOW) 382 { 383 result.put("message", "not-allowed"); 384 return result; 385 } 386 387 for (FormEntry entry: form.getEntries()) 388 { 389 entryIds.add(entry.getId()); 390 entry.remove(); 391 } 392 form.saveChanges(); 393 Map<String, Object> eventParams = new HashMap<>(); 394 eventParams.put("form", form); 395 _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _currentUserProvider.getUser(), eventParams)); 396 397 result.put("ids", entryIds); 398 result.put("formId", form.getId()); 399 400 return result; 401 } 402}