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}