001/*
002 *  Copyright 2010 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.workflow;
017
018import java.util.ArrayList;
019import java.util.HashMap;
020import java.util.HashSet;
021import java.util.List;
022import java.util.Map;
023import java.util.Set;
024
025import org.apache.commons.lang.StringUtils;
026import org.apache.excalibur.xml.xpath.PrefixResolver;
027import org.apache.excalibur.xml.xpath.XPathProcessor;
028import org.w3c.dom.NamedNodeMap;
029import org.w3c.dom.Node;
030import org.w3c.dom.NodeList;
031
032import org.ametys.core.util.URIUtils;
033import org.ametys.plugins.forms.Field;
034import org.ametys.plugins.forms.Field.FieldType;
035import org.ametys.plugins.forms.Fieldset;
036import org.ametys.plugins.forms.Form;
037
038/**
039 * Form parser.
040 */
041public class FormParser
042{
043
044    /** The rich text prefix resolver. */
045    public static final PrefixResolver RICH_TEXT_PREFIX_RESOLVER = new RichTextPrefixResolver();
046    
047    /** The label XML element name. */
048    protected static final String _LABEL_ELEMENT = "label";
049    
050    /** The input XML element name. */
051    protected static final String _INPUT_ELEMENT = "input";
052    
053    /** The textarea XML element name. */
054    protected static final String _TEXTAREA_ELEMENT = "textarea";
055    
056    /** The select XML element name. */
057    protected static final String _SELECT_ELEMENT = "select";
058    
059    /** The option XML element name. */
060    protected static final String _OPTION_ELEMENT = "option";
061    
062    /** The captcha XML element name. */
063    protected static final String _CAPTCHA_ELEMENT = "captcha";
064    
065    /** The fieldset XML element name. */
066    protected static final String _FIELDSET_ELEMENT = "fieldset";
067    
068    /** The fieldset XML element name. */
069    protected static final String _LEGEND_ELEMENT = "legend";
070    
071    /** An XPath processor. */
072    protected XPathProcessor _xpathProcessor;
073    
074    /**
075     * Default constructor.
076     * @param xpathProcessor the xpath processor
077     */
078    public FormParser(XPathProcessor xpathProcessor)
079    {
080        _xpathProcessor = xpathProcessor;
081    }
082
083    /**
084     * Return a list of nodes selected by an xpath expression.
085     * @param context the context node.
086     * @param xpathExpression the expression used to select nodes.
087     * @return a list of nodes selected by an xpath expression.
088     */
089    public List<Node> getNodesAsList(Node context, String xpathExpression)
090    {
091        NodeList selectNodeList = _xpathProcessor.selectNodeList(context, xpathExpression, RICH_TEXT_PREFIX_RESOLVER);
092        ArrayList<Node> toReturn = null;
093        if (selectNodeList != null)
094        {
095            toReturn = new ArrayList<>();
096            for (int i = 0; i < selectNodeList.getLength(); i++)
097            {
098                toReturn.add(selectNodeList.item(i));
099            }
100        }
101        return toReturn;
102    }
103
104    /**
105     * Parses the form.
106     * @param formNode the node to parse (must be a node &lt;html:form type="cms"&gt;)
107     * @return the extracted Form.
108     */
109    public Form parseForm(Node formNode)
110    {
111        if (!formNode.getNodeName().equals("form") && !"cms".equals(_getAttribute(formNode, "type")))
112        {
113            throw new IllegalArgumentException("The form node must be of type <form type='cms'>");
114        }
115        
116        Form form = new Form();
117        
118        String id = StringUtils.defaultString(_getAttribute(formNode, "id"));
119        String label = StringUtils.defaultString(_getAttribute(formNode, "label"));
120        String receiptFieldId = StringUtils.defaultString(_getAttribute(formNode, "receipt_to"));
121        String receiptFieldFromAddress = StringUtils.defaultString(_getAttribute(formNode, "receipt_from"));
122        String receiptFieldSubject = StringUtils.defaultString(_getAttribute(formNode, "receipt_subject"));
123        String receiptFieldBody = StringUtils.defaultString(_getAttribute(formNode, "receipt_body"));
124        String redirectTo = StringUtils.defaultString(_getAttribute(formNode, "redirect"));
125        String emails = StringUtils.defaultString(_getAttribute(formNode, "processing_emails"));
126        String workflowName = StringUtils.defaultString(_getAttribute(formNode, "workflow"));
127        String limit = StringUtils.defaultString(_getAttribute(formNode, "limit"));
128        String places = StringUtils.defaultString(_getAttribute(formNode, "places"));
129        String noPlaces = StringUtils.defaultString(_getAttribute(formNode, "no_places"));
130        
131        emails = URIUtils.decode(emails);
132        
133        Set<String> emailSet = new HashSet<>();
134        for (String email : emails.split("[;, \n]"))
135        {
136            if (StringUtils.isNotBlank(email))
137            {
138                emailSet.add(email.trim());
139            }
140        }
141        
142        _parseFields(formNode, form);
143        
144        form.setId(id);
145        form.setLabel(label);
146        form.setReceiptFieldId(receiptFieldId);
147        form.setReceiptFieldFromAddress(receiptFieldFromAddress);
148        form.setReceiptFieldSubject(receiptFieldSubject);
149        form.setReceiptFieldBody(receiptFieldBody);
150        form.setNotificationEmails(emailSet);
151        form.setRedirectTo(redirectTo);
152        form.setWorkflowName(workflowName);
153        form.setLimit(limit);
154        form.setRemainingPlaces(places);
155        form.setNoRemainingPlaces(noPlaces);
156        
157        return form;
158    }
159    
160    /**
161     * Return the value of an attribute of an input
162     * 
163     * @param node the node
164     * @param attributeName the name of the attribute
165     * @return the value of an attribute of an input
166     */
167    public String _getAttribute(Node node, String attributeName)
168    {
169        NamedNodeMap attributes = node.getAttributes();
170        Node idAttribute = attributes.getNamedItem(attributeName);
171        String toReturn;
172        if (idAttribute != null)
173        {
174            toReturn = idAttribute.getNodeValue();
175        }
176        else
177        {
178            toReturn = null;
179        }
180        return toReturn;
181    }
182
183    /**
184     * Parse the fields of a form.
185     * @param formNode the form node.
186     * @param form the form object.
187     */
188    protected void _parseFields(Node formNode, Form form)
189    {
190        List<Field> fields = new ArrayList<>();
191        List<Fieldset> fieldsets = new ArrayList<>();
192        
193        Map<String, String> labels = new HashMap<>();
194        
195        NodeList nodeList = _xpathProcessor.selectNodeList(formNode, "descendant::html:*", RICH_TEXT_PREFIX_RESOLVER);
196        for (int i = 0; i < nodeList.getLength(); i++)
197        {
198            Node node = nodeList.item(i);
199            String localName = node.getLocalName();
200            
201            if (_LABEL_ELEMENT.equals(localName))
202            {
203                _processLabel(node, labels);
204            }
205            else if (_INPUT_ELEMENT.equals(localName))
206            {
207                Field field = _processInput(node);
208                if (field != null)
209                {
210                    fields.add(field);
211                }
212            }
213            else if (_TEXTAREA_ELEMENT.equals(localName))
214            {
215                Field field = _processTextarea(node);
216                if (field != null)
217                {
218                    fields.add(field);
219                }
220            }
221            else if (_SELECT_ELEMENT.equals(localName))
222            {
223                Field field = _processSelect(node);
224                if (field != null)
225                {
226                    fields.add(field);
227                }
228            }
229            else if (_CAPTCHA_ELEMENT.equals(localName))
230            {
231                Field field = _processCaptcha(node);
232                if (field != null)
233                {
234                    fields.add(field);
235                }
236            }
237            else if (_FIELDSET_ELEMENT.equals(localName))
238            {
239                Fieldset fieldset = _processFieldset(node);
240                if (fieldset != null)
241                {
242                    fieldsets.add(fieldset);
243                }
244            }
245        }
246        
247        // Fill in the field labels with the extracted labels.
248        for (Field field : fields)
249        {
250            if (labels.containsKey(field.getId()))
251            {
252                field.setLabel(labels.get(field.getId()));
253            }
254        }
255        
256        form.setFields(fields);
257        form.setFieldsets(fieldsets);
258    }
259    
260    /**
261     * Process a fieldset.
262     * @param fieldsetNode the fielset DOM node.
263     * @return the fieldset.
264     */
265    protected Fieldset _processFieldset(Node fieldsetNode)
266    {
267        Fieldset fieldset = new Fieldset();
268        
269        Node legendNode = fieldsetNode.getFirstChild();
270        if (_LEGEND_ELEMENT.equals(legendNode.getLocalName()))
271        {
272            fieldset.setLabel(legendNode.getTextContent());
273        }
274        
275        List<Node> fieldNodes = getNodesAsList(fieldsetNode, "descendant::html:*");
276        for (Node fieldNode : fieldNodes)
277        {
278            String id = _getAttribute(fieldNode, "id");
279            if (StringUtils.isNotEmpty(id))
280            {
281                fieldset.getFieldIds().add(id);
282            }
283        }
284        
285        return fieldset;
286    }
287
288    /**
289     * Process a label.
290     * @param labelNode the label DOM node.
291     * @param labels the label map.
292     */
293    protected void _processLabel(Node labelNode, Map<String, String> labels)
294    {
295        String id = _getAttribute(labelNode, "for");
296        String label = labelNode.getTextContent().trim();
297        // Trim the ending colon if present.
298        if (label.endsWith(":"))
299        {
300            label = label.substring(0, label.length() - 1).trim();
301        }
302        
303        if (StringUtils.isNotEmpty(id))
304        {
305            labels.put(id, label);
306        }
307    }
308    
309    /**
310     * Process an input.
311     * @param inputNode the input DOM node.
312     * @return the input as a Field.
313     */
314    protected Field _processInput(Node inputNode)
315    {
316        Field field = null;
317        
318        FieldType type = _getInputType(inputNode);
319        
320        if (type != null)
321        {
322            field = new Field(type);
323            
324            _processAttributes(inputNode, field);
325        }
326        
327        return field;
328    }
329
330    /**
331     * Process an textarea.
332     * @param textareaNode the textarea DOM node.
333     * @return the textarea as a Field.
334     */
335    protected Field _processTextarea(Node textareaNode)
336    {
337        Field field = new Field(FieldType.TEXTAREA);
338        
339        _processAttributes(textareaNode, field);
340        
341        return field;
342    }
343
344    /**
345     * Process a select.
346     * @param selectNode the select DOM node.
347     * @return the select as a Field.
348     */
349    protected Field _processSelect(Node selectNode)
350    {
351        Field field = new Field(FieldType.SELECT);
352        
353        _processAttributes(selectNode, field);
354        
355        Map<String, String> optionProperties = new HashMap<>();
356        
357        List<Node> optionNodes = getNodesAsList(selectNode, "html:" + _OPTION_ELEMENT);
358        
359        int i = 0;
360        for (Node optionNode : optionNodes)
361        {
362            String value = _getAttribute(optionNode, "value");
363            String label = optionNode.getTextContent();
364            
365            optionProperties.put("option-" + i + "-value", value);
366            optionProperties.put("option-" + i + "-label", label);
367            i++;
368        }
369        
370        field.getProperties().putAll(optionProperties);
371        
372        return field;
373    }
374    
375    /**
376     * Process a captcha.
377     * @param captchaNode the captcha DOM node.
378     * @return the captcha as a Field.
379     */
380    protected Field _processCaptcha(Node captchaNode)
381    {
382        Field field = new Field(FieldType.CAPTCHA);
383        
384        _processAttributes(captchaNode, field);
385        
386        return field;
387    }
388    
389    /**
390     * Get the input type.
391     * @param inputNode the input node.
392     * @return the field type.
393     */
394    protected FieldType _getInputType(Node inputNode)
395    {
396        FieldType type = null;
397        
398        String typeAttr = _getAttribute(inputNode, "type");
399        if ("text".equals(typeAttr))
400        {
401            type = FieldType.TEXT;
402        }
403        else if ("hidden".equals(typeAttr))
404        {
405            type = FieldType.HIDDEN;
406        }
407        else if ("password".equals(typeAttr))
408        {
409            type = FieldType.PASSWORD;
410        }
411        else if ("checkbox".equals(typeAttr))
412        {
413            type = FieldType.CHECKBOX;
414        }
415        else if ("radio".equals(typeAttr))
416        {
417            type = FieldType.RADIO;
418        }
419        else if ("file".equals(typeAttr))
420        {
421            type = FieldType.FILE;
422        }
423        
424        return type;
425    }
426    
427    /**
428     * Process common field attributes and properties.
429     * @param node the field node.
430     * @param field the field object to fill.
431     */
432    protected void _processAttributes(Node node, Field field)
433    {
434        String id = _getAttribute(node, "id");
435        String name = _getAttribute(node, "name");
436        
437        Map<String, String> properties = new HashMap<>();
438        NamedNodeMap atts = node.getAttributes();
439        for (int i = 0; i < atts.getLength(); i++)
440        {
441            Node attribute = atts.item(i);
442            String attrName = attribute.getLocalName();
443            if (!"id".equals(attrName) && !"name".equals(attrName) && !"type".equals(attrName))
444            {
445                String value = attribute.getNodeValue();
446                properties.put(attrName, value);
447            }
448        }
449        
450        field.setId(id);
451        field.setName(name);
452        field.getProperties().putAll(properties);
453    }
454    
455    /**
456     * XML prefix resolver which declares docbook and html namespaces.
457     */
458    public static class RichTextPrefixResolver implements PrefixResolver
459    {
460        /**
461         * The declared XML namespaces.
462         */
463        public static final Map<String, String> NAMESPACES = new HashMap<>();
464        
465        static
466        {
467            NAMESPACES.put("docbook", "http://docbook.org/ns/docbook");
468            NAMESPACES.put("html", "http://www.w3.org/1999/xhtml");
469            NAMESPACES.put("xlink", "http://www.w3.org/1999/xlink");
470        }
471        
472        public String prefixToNamespace(String prefix)
473        {
474            return NAMESPACES.get(prefix);
475        }
476    }
477}