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