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