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}