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 try 216 { 217 // Parse the form node. 218 Form form = _formParser.parseForm(formNode); 219 forms.add(form); 220 221 // Try to retrieve the old form properties from the repository. 222 Form oldForm = getFormPropertiesManager().getForm(siteName, form.getId()); 223 224 String newWorkflowName = StringUtils.defaultString(form.getWorkflowName()); 225 if (oldForm != null) 226 { 227 String oldWorkflowName = oldForm.getWorkflowName(); 228 if (!newWorkflowName.equals(StringUtils.defaultString(oldWorkflowName))) 229 { 230 // The workflow has switched 231 boolean dropColumn = StringUtils.isEmpty(newWorkflowName) && getFormTableManager().hasWorkflowIdColumn(form.getId()); 232 boolean addColumn = StringUtils.isNotEmpty(newWorkflowName) && !getFormTableManager().hasWorkflowIdColumn(form.getId()); 233 234 // update/delete the concerned tables 235 _resetWorkflowTables(content, form, dropColumn, addColumn); 236 } 237 } 238 239 // TODO Give oldForm to the form table manager to compare old/new state (in particular for radio buttons.) 240 241 // Create the table only if it was given a label. 242 // Otherwise, the results will be sent by e-mail and won't be browsable. 243 if (StringUtils.isNotBlank(form.getLabel())) 244 { 245 if (!getFormTableManager().createTable(form)) 246 { 247 getLogger().error("The form " + form.getLabel() + " was not created in the database."); 248 } 249 } 250 251 if (oldForm == null) 252 { 253 getFormPropertiesManager().createForm(siteName, form, content); 254 if (StringUtils.isNotEmpty(newWorkflowName)) 255 { 256 getFormTableManager().addWorkflowIdColumn(form.getId()); 257 } 258 } 259 else 260 { 261 getFormPropertiesManager().updateForm(siteName, form, content); 262 } 263 } 264 catch (FormsException e) 265 { 266 // Form error. 267 getLogger().error("Error trying to store a form in the content " + contentName + " (" + content.getId() + ")", e); 268 } 269 catch (SQLException e) 270 { 271 // SQL error. 272 getLogger().error("Error trying to store a form in the content " + contentName + " (" + content.getId() + ")", e); 273 } 274 } 275 } 276 277 _removeUnusedForms(content, contentName, forms); 278 279 if (getLogger().isDebugEnabled()) 280 { 281 getLogger().debug("Forms processed for content '" + contentName + "'"); 282 } 283 } 284 285 /** 286 * Remove the workflow tables and the workflow id column if needed 287 * @param form the form 288 * @param dropColumn true to drop the workflow id column 289 * @param addColumn true to add the workflow id column 290 * @throws FormsException if an error occurs while retrieving the workflow instances ids 291 * @throws WorkflowException if an error occurs during the reset of the workflow of the form entries 292 * @throws SQLException if an error occurs during the SQL queries 293 */ 294 private void _resetWorkflowTables(Content content, Form form, boolean dropColumn, boolean addColumn) throws FormsException, WorkflowException, SQLException 295 { 296 // Add the column just now to get the proper submissions 297 if (addColumn) 298 { 299 getFormTableManager().addWorkflowIdColumn(form.getId()); 300 } 301 302 Map<String, FieldValue> columns = getFormTableManager().getColumns(form); 303 List<UserEntry> submissions = getFormTableManager().getSubmissions(form, columns, 0, Integer.MAX_VALUE, null); 304 305 // Delete the corresponding workflow instances and their history 306 Workflow workflow = getWorkflowProvider().getExternalWorkflow(JdbcWorkflowStore.ROLE); 307 for (UserEntry submission : submissions) 308 { 309 Integer workflowId = submission.getWorkflowId(); 310 if (workflowId != null && workflowId != 0) 311 { 312 getJdbcWorkflowStore().clearHistory(workflowId); 313 getJdbcWorkflowStore().deleteInstance(workflowId); 314 } 315 316 if (getFormTableManager().hasWorkflowIdColumn(form.getId()) && !dropColumn) 317 { 318 String workflowName = form.getWorkflowName(); 319 int initialActionId = getWorkflowHelper().getInitialAction(workflowName); 320 321 Map<String, Object> inputs = new HashMap<>(); 322 inputs.put("formId", form.getId()); 323 inputs.put("entryId", String.valueOf(submission.getId())); 324 inputs.put("contentId", content.getId()); 325 inputs.put(ENTRY_WORKFLOW_REINITIALIZATION, true); 326 inputs.put(ObjectModelHelper.PARENT_CONTEXT, _context); 327 inputs.put(ObjectModelHelper.REQUEST_OBJECT, ContextHelper.getRequest(_context)); 328 long newWorkflowId = workflow.initialize(workflowName, initialActionId, inputs); 329 getFormTableManager().setWorkflowId(form, submission.getId(), newWorkflowId); 330 } 331 } 332 333 if (dropColumn) 334 { 335 getFormTableManager().dropWorkflowIdColumn(form.getId()); 336 } 337 } 338 339 /** 340 * Remove the unused forms 341 * @param content The content 342 * @param contentName The name of the content 343 * @param forms The forms submitted 344 */ 345 private void _removeUnusedForms(Content content, String contentName, List<Form> forms) 346 { 347 try 348 { 349 for (Form form : getFormPropertiesManager().getForms(content)) 350 { 351 boolean found = false; 352 for (Form form2 : forms) 353 { 354 if (form2.getId().equals(form.getId())) 355 { 356 found = true; 357 break; 358 } 359 } 360 361 if (!found) 362 { 363 getFormPropertiesManager().remove(form, content); 364 } 365 } 366 } 367 catch (FormsException e) 368 { 369 // Form error. 370 getLogger().error("Cannot iterate on existing forms to remove unused forms on content " + contentName + " (" + content.getId() + ")", e); 371 } 372 } 373 374 /** 375 * Get the rich texts 376 * @param metadataHolder the metadata holder 377 * @return the rich texts in a Set 378 */ 379 protected Set<RichText> _getRichTexts (CompositeMetadata metadataHolder) 380 { 381 Set<RichText> richTexts = new HashSet<>(); 382 383 String[] metadataNames = metadataHolder.getMetadataNames(); 384 for (String metadataName : metadataNames) 385 { 386 org.ametys.plugins.repository.metadata.CompositeMetadata.MetadataType type = metadataHolder.getType(metadataName); 387 if (org.ametys.plugins.repository.metadata.CompositeMetadata.MetadataType.RICHTEXT.equals(type)) 388 { 389 richTexts.add(metadataHolder.getRichText(metadataName)); 390 } 391 else if (org.ametys.plugins.repository.metadata.CompositeMetadata.MetadataType.COMPOSITE.equals(type)) 392 { 393 richTexts.addAll(_getRichTexts(metadataHolder.getCompositeMetadata(metadataName))); 394 } 395 } 396 397 return richTexts; 398 } 399}