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