001/* 002 * Copyright 2011 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; 017 018import java.io.IOException; 019import java.io.InputStream; 020import java.sql.SQLException; 021import java.util.ArrayList; 022import java.util.Arrays; 023import java.util.HashMap; 024import java.util.HashSet; 025import java.util.List; 026import java.util.Map; 027import java.util.Set; 028 029import org.apache.avalon.framework.component.Component; 030import org.apache.avalon.framework.context.Context; 031import org.apache.avalon.framework.context.ContextException; 032import org.apache.avalon.framework.context.Contextualizable; 033import org.apache.avalon.framework.logger.AbstractLogEnabled; 034import org.apache.avalon.framework.service.ServiceException; 035import org.apache.avalon.framework.service.ServiceManager; 036import org.apache.avalon.framework.service.Serviceable; 037import org.apache.cocoon.components.ContextHelper; 038import org.apache.cocoon.environment.ObjectModelHelper; 039import org.apache.commons.lang.StringUtils; 040import org.apache.excalibur.xml.dom.DOMParser; 041import org.apache.excalibur.xml.xpath.XPathProcessor; 042import org.w3c.dom.Document; 043import org.w3c.dom.Node; 044import org.xml.sax.InputSource; 045import org.xml.sax.SAXException; 046 047import org.ametys.cms.data.RichText; 048import org.ametys.cms.data.type.ModelItemTypeConstants; 049import org.ametys.cms.repository.Content; 050import org.ametys.plugins.forms.data.FieldValue; 051import org.ametys.plugins.forms.data.UserEntry; 052import org.ametys.plugins.forms.jcr.FormPropertiesManager; 053import org.ametys.plugins.forms.table.FormTableManager; 054import org.ametys.plugins.forms.workflow.FormParser; 055import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder; 056import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper; 057import org.ametys.plugins.workflow.store.JdbcWorkflowStore; 058import org.ametys.plugins.workflow.support.WorkflowHelper; 059import org.ametys.plugins.workflow.support.WorkflowProvider; 060import org.ametys.web.repository.content.WebContent; 061 062import com.opensymphony.workflow.Workflow; 063import com.opensymphony.workflow.WorkflowException; 064 065/** 066 * Class to handle forms in contents 067 */ 068public class FormManager extends AbstractLogEnabled implements Component, Serviceable, Contextualizable 069{ 070 /** Avalon Role */ 071 public static final String ROLE = FormManager.class.getName(); 072 073 /** Constant for entry workflow reinitialization */ 074 public static final String ENTRY_WORKFLOW_REINITIALIZATION = "workflowEntryReinitialization"; 075 076 077 private static final String __FORM_TYPE_CMS = "//html:form[@type='cms']"; 078 079 private ServiceManager _manager; 080 081 private FormPropertiesManager _formPropertiesManager; 082 private DOMParser _parser; 083 private FormParser _formParser; 084 private FormTableManager _formTableManager; 085 private WorkflowProvider _workflowProvider; 086 private JdbcWorkflowStore _jdbcWorkflowStore; 087 private WorkflowHelper _workflowHelper; 088 private Context _context; 089 090 @Override 091 public void service(ServiceManager smanager) throws ServiceException 092 { 093 _manager = smanager; 094 _parser = (DOMParser) smanager.lookup(DOMParser.ROLE); 095 XPathProcessor processor = (XPathProcessor) smanager.lookup(XPathProcessor.ROLE); 096 _formParser = new FormParser(processor); 097 } 098 099 public void contextualize(Context context) throws ContextException 100 { 101 _context = context; 102 } 103 104 private FormPropertiesManager getFormPropertiesManager() 105 { 106 if (_formPropertiesManager == null) 107 { 108 try 109 { 110 _formPropertiesManager = (FormPropertiesManager) _manager.lookup(FormPropertiesManager.ROLE); 111 } 112 catch (ServiceException e) 113 { 114 throw new RuntimeException(e); 115 } 116 } 117 return _formPropertiesManager; 118 } 119 120 private FormTableManager getFormTableManager() 121 { 122 if (_formTableManager == null) 123 { 124 try 125 { 126 _formTableManager = (FormTableManager) _manager.lookup(FormTableManager.ROLE); 127 } 128 catch (ServiceException e) 129 { 130 throw new RuntimeException(e); 131 } 132 } 133 return _formTableManager; 134 } 135 136 private WorkflowProvider getWorkflowProvider() 137 { 138 if (_workflowProvider == null) 139 { 140 try 141 { 142 _workflowProvider = (WorkflowProvider) _manager.lookup(WorkflowProvider.ROLE); 143 } 144 catch (ServiceException e) 145 { 146 throw new RuntimeException(e); 147 } 148 } 149 return _workflowProvider; 150 } 151 152 private WorkflowHelper getWorkflowHelper() 153 { 154 if (_workflowHelper == null) 155 { 156 try 157 { 158 _workflowHelper = (WorkflowHelper) _manager.lookup(WorkflowHelper.ROLE); 159 } 160 catch (ServiceException e) 161 { 162 throw new RuntimeException(e); 163 } 164 } 165 return _workflowHelper; 166 } 167 168 private JdbcWorkflowStore getJdbcWorkflowStore() 169 { 170 if (_jdbcWorkflowStore == null) 171 { 172 try 173 { 174 _jdbcWorkflowStore = (JdbcWorkflowStore) _manager.lookup(JdbcWorkflowStore.ROLE); 175 } 176 catch (ServiceException e) 177 { 178 throw new RuntimeException(e); 179 } 180 } 181 return _jdbcWorkflowStore; 182 } 183 184 /** 185 * Find all the forms the content contains and process them. 186 * @param content the content. 187 * @throws SAXException if a SAX exception occurs during content parsing. 188 * @throws IOException if an I/O exception occurs during content parsing. 189 * @throws WorkflowException if an exception occurs while handling the form's workflow 190 */ 191 public void processContentForms(Content content) throws SAXException, IOException, WorkflowException 192 { 193 String contentName = content.getName(); 194 String siteName = ""; 195 if (content instanceof WebContent) 196 { 197 siteName = ((WebContent) content).getSiteName(); 198 } 199 200 if (getLogger().isDebugEnabled()) 201 { 202 getLogger().debug("Processing the forms for content '" + contentName + "'"); 203 } 204 205 List<Form> forms = new ArrayList<>(); 206 207 Set<RichText> richTexts = _getRichTexts(content); 208 for (RichText richText : richTexts) 209 { 210 InputStream contentStream = richText.getInputStream(); 211 212 Document document = _parser.parseDocument(new InputSource(contentStream)); 213 214 List<Node> formNodes = _formParser.getNodesAsList(document, __FORM_TYPE_CMS); 215 216 for (Node formNode : formNodes) 217 { 218 _processForm(content, contentName, siteName, forms, formNode); 219 } 220 } 221 222 _removeUnusedForms(content, contentName, forms); 223 224 if (getLogger().isDebugEnabled()) 225 { 226 getLogger().debug("Forms processed for content '" + contentName + "'"); 227 } 228 } 229 230 private void _processForm(Content content, String contentName, String siteName, List<Form> forms, Node formNode) throws WorkflowException 231 { 232 try 233 { 234 // Parse the form node. 235 Form form = _formParser.parseForm(formNode); 236 forms.add(form); 237 238 // Try to retrieve the old form properties from the repository. 239 Form oldForm = getFormPropertiesManager().getForm(siteName, form.getId()); 240 241 String newWorkflowName = StringUtils.defaultString(form.getWorkflowName()); 242 if (oldForm != null) 243 { 244 String oldWorkflowName = oldForm.getWorkflowName(); 245 if (!newWorkflowName.equals(StringUtils.defaultString(oldWorkflowName))) 246 { 247 // The workflow has switched 248 boolean dropColumn = StringUtils.isEmpty(newWorkflowName) && getFormTableManager().hasWorkflowIdColumn(form.getId()); 249 boolean addColumn = StringUtils.isNotEmpty(newWorkflowName) && !getFormTableManager().hasWorkflowIdColumn(form.getId()); 250 251 // update/delete the concerned tables 252 _resetWorkflowTables(content, form, dropColumn, addColumn); 253 } 254 } 255 256 // TODO Give oldForm to the form table manager to compare old/new state (in particular for radio buttons.) 257 258 // Create the table only if it was given a label. 259 // Otherwise, the results will be sent by e-mail and won't be browsable. 260 if (StringUtils.isNotBlank(form.getLabel())) 261 { 262 if (!getFormTableManager().createTable(form)) 263 { 264 getLogger().error("The form " + form.getLabel() + " was not created in the database."); 265 } 266 } 267 268 if (oldForm == null) 269 { 270 getFormPropertiesManager().createForm(siteName, form, content); 271 if (StringUtils.isNotEmpty(newWorkflowName)) 272 { 273 getFormTableManager().addWorkflowIdColumn(form.getId()); 274 } 275 } 276 else 277 { 278 getFormPropertiesManager().updateForm(siteName, form, content); 279 } 280 } 281 catch (FormsException e) 282 { 283 // Form error. 284 getLogger().error("Error trying to store a form in the content " + contentName + " (" + content.getId() + ")", e); 285 } 286 catch (SQLException e) 287 { 288 // SQL error. 289 getLogger().error("Error trying to store a form in the content " + contentName + " (" + content.getId() + ")", e); 290 } 291 } 292 293 /** 294 * Remove the workflow tables and the workflow id column if needed 295 * @param content The content holding the form 296 * @param form the form 297 * @param dropColumn true to drop the workflow id column 298 * @param addColumn true to add the workflow id column 299 * @throws FormsException if an error occurs while retrieving the workflow instances ids 300 * @throws WorkflowException if an error occurs during the reset of the workflow of the form entries 301 * @throws SQLException if an error occurs during the SQL queries 302 */ 303 private void _resetWorkflowTables(Content content, Form form, boolean dropColumn, boolean addColumn) throws FormsException, WorkflowException, SQLException 304 { 305 // Add the column just now to get the proper submissions 306 if (addColumn) 307 { 308 getFormTableManager().addWorkflowIdColumn(form.getId()); 309 } 310 311 Map<String, FieldValue> columns = getFormTableManager().getColumns(form); 312 List<UserEntry> submissions = getFormTableManager().getSubmissions(form, columns, 0, Integer.MAX_VALUE, null); 313 314 // Delete the corresponding workflow instances and their history 315 Workflow workflow = getWorkflowProvider().getExternalWorkflow(JdbcWorkflowStore.ROLE); 316 for (UserEntry submission : submissions) 317 { 318 Integer workflowId = submission.getWorkflowId(); 319 if (workflowId != null && workflowId != 0) 320 { 321 getJdbcWorkflowStore().clearHistory(workflowId); 322 getJdbcWorkflowStore().deleteInstance(workflowId); 323 } 324 325 if (getFormTableManager().hasWorkflowIdColumn(form.getId()) && !dropColumn) 326 { 327 String workflowName = form.getWorkflowName(); 328 int initialActionId = getWorkflowHelper().getInitialAction(workflowName); 329 330 Map<String, Object> inputs = new HashMap<>(); 331 inputs.put("formId", form.getId()); 332 inputs.put("entryId", String.valueOf(submission.getId())); 333 inputs.put("contentId", content.getId()); 334 inputs.put(ENTRY_WORKFLOW_REINITIALIZATION, true); 335 inputs.put(ObjectModelHelper.PARENT_CONTEXT, _context); 336 inputs.put(ObjectModelHelper.REQUEST_OBJECT, ContextHelper.getRequest(_context)); 337 long newWorkflowId = workflow.initialize(workflowName, initialActionId, inputs); 338 getFormTableManager().setWorkflowId(form, submission.getId(), newWorkflowId); 339 } 340 } 341 342 if (dropColumn) 343 { 344 getFormTableManager().dropWorkflowIdColumn(form.getId()); 345 } 346 } 347 348 /** 349 * Remove the unused forms 350 * @param content The content 351 * @param contentName The name of the content 352 * @param forms The forms submitted 353 */ 354 private void _removeUnusedForms(Content content, String contentName, List<Form> forms) 355 { 356 try 357 { 358 for (Form form : getFormPropertiesManager().getForms(content)) 359 { 360 boolean found = false; 361 for (Form form2 : forms) 362 { 363 if (form2.getId().equals(form.getId())) 364 { 365 found = true; 366 break; 367 } 368 } 369 370 if (!found) 371 { 372 getFormPropertiesManager().remove(form, content); 373 } 374 } 375 } 376 catch (FormsException e) 377 { 378 // Form error. 379 getLogger().error("Cannot iterate on existing forms to remove unused forms on content " + contentName + " (" + content.getId() + ")", e); 380 } 381 } 382 383 /** 384 * Get the rich texts 385 * @param dataHolder the data holder 386 * @return the rich texts in a Set 387 */ 388 protected Set<RichText> _getRichTexts (ModelAwareDataHolder dataHolder) 389 { 390 Set<RichText> richTexts = new HashSet<>(); 391 392 Map<String, Object> richTextValues = DataHolderHelper.findItemsByType(dataHolder, ModelItemTypeConstants.RICH_TEXT_ELEMENT_TYPE_ID); 393 for (Object richTextValue : richTextValues.values()) 394 { 395 if (richTextValue instanceof RichText[]) 396 { 397 Arrays.stream((RichText[]) richTextValue) 398 .forEach(richTexts::add); 399 } 400 else if (richTextValue != null) // Test that the rich text value is not empty 401 { 402 richTexts.add((RichText) richTextValue); 403 } 404 } 405 406 return richTexts; 407 } 408}