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