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 <html:form type="cms">) 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}