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