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