001/** 002 * 003 * Copyright 2014 Anno van Vliet, 2019-2020 Florian Schmaus 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.jivesoftware.smackx.xdatavalidation.packet; 018 019import java.math.BigInteger; 020 021import javax.xml.namespace.QName; 022 023import org.jivesoftware.smack.datatypes.UInt32; 024import org.jivesoftware.smack.packet.XmlElement; 025import org.jivesoftware.smack.packet.XmlEnvironment; 026import org.jivesoftware.smack.util.StringUtils; 027import org.jivesoftware.smack.util.XmlStringBuilder; 028 029import org.jivesoftware.smackx.xdata.AbstractSingleStringValueFormField; 030import org.jivesoftware.smackx.xdata.FormField; 031import org.jivesoftware.smackx.xdata.FormFieldChildElement; 032import org.jivesoftware.smackx.xdata.packet.DataForm; 033import org.jivesoftware.smackx.xdatavalidation.ValidationConsistencyException; 034 035/** 036 * DataValidation Extension according to XEP-0122: Data Forms Validation. This specification defines a 037 * backwards-compatible extension to the XMPP Data Forms protocol that enables applications to specify additional 038 * validation guidelines related to a {@link FormField} in a {@link DataForm}, such as validation of standard XML 039 * datatypes, application-specific datatypes, value ranges, and regular expressions. 040 * 041 * @author Anno van Vliet 042 */ 043public abstract class ValidateElement implements FormFieldChildElement { 044 045 public static final String DATATYPE_XS_STRING = "xs:string"; 046 public static final String ELEMENT = "validate"; 047 public static final String NAMESPACE = "http://jabber.org/protocol/xdata-validate"; 048 049 public static final QName QNAME = new QName(NAMESPACE, ELEMENT); 050 051 private final String datatype; 052 053 private ListRange listRange; 054 055 /** 056 * The 'datatype' attribute specifies the datatype. This attribute is OPTIONAL, and when not specified, defaults to 057 * "xs:string". 058 * 059 * @param datatype the data type of any value contained within the {@link FormField} element. 060 */ 061 private ValidateElement(String datatype) { 062 this.datatype = StringUtils.isNotEmpty(datatype) ? datatype : null; 063 } 064 065 /** 066 * Specifies the data type of any value contained within the {@link FormField} element. It MUST meet one of the 067 * following conditions: 068 * <ul> 069 * <li>Start with "xs:", and be one of the "built-in" datatypes defined in XML Schema Part 2 <a 070 * href="http://www.xmpp.org/extensions/xep-0122.html#nt-idp1476016">[2]</a></li> 071 * <li>Start with a prefix registered with the XMPP Registrar <a 072 * href="http://www.xmpp.org/extensions/xep-0122.html#nt-idp1478544">[3]</a></li> 073 * <li>Start with "x:", and specify a user-defined datatype <a 074 * href="http://www.xmpp.org/extensions/xep-0122.html#nt-idp1477360">[4]</a></li> 075 * </ul> 076 * 077 * @return the datatype 078 */ 079 public String getDatatype() { 080 return datatype != null ? datatype : DATATYPE_XS_STRING; 081 } 082 083 @Override 084 public String getElementName() { 085 return ELEMENT; 086 } 087 088 @Override 089 public String getNamespace() { 090 return NAMESPACE; 091 } 092 093 @Override 094 public QName getQName() { 095 return QNAME; 096 } 097 098 @Override 099 public final boolean mustBeOnlyOfHisKind() { 100 return true; 101 } 102 103 @Override 104 public XmlStringBuilder toXML(org.jivesoftware.smack.packet.XmlEnvironment enclosingNamespace) { 105 XmlStringBuilder buf = new XmlStringBuilder(this, enclosingNamespace); 106 buf.optAttribute("datatype", datatype); 107 buf.rightAngleBracket(); 108 appendXML(buf); 109 buf.optAppend(getListRange()); 110 buf.closeElement(this); 111 return buf; 112 } 113 114 /** 115 * Append XML. 116 * 117 * @param buf TODO javadoc me please 118 */ 119 protected abstract void appendXML(XmlStringBuilder buf); 120 121 /** 122 * Set list range. 123 * @param listRange the listRange to set 124 */ 125 public void setListRange(ListRange listRange) { 126 this.listRange = listRange; 127 } 128 129 /** 130 * Get list range. 131 * @return the listRange 132 */ 133 public ListRange getListRange() { 134 return listRange; 135 } 136 137 /** 138 * Check if this element is consistent according to the business rules in XEP-0122. 139 * 140 * @param formFieldBuilder the builder used to construct the form field. 141 */ 142 @Override 143 public abstract void checkConsistency(FormField.Builder<?, ?> formFieldBuilder); 144 145 public static ValidateElement from(FormField formField) { 146 return (ValidateElement) formField.getFormFieldChildElement(QNAME); 147 } 148 149 /** 150 * Validation only against the datatype itself. Indicates that the value(s) should simply match the field type and 151 * datatype constraints. 152 * 153 * @see ValidateElement 154 */ 155 public static class BasicValidateElement extends ValidateElement { 156 157 public static final String METHOD = "basic"; 158 159 /** 160 * Basic validate element constructor. 161 * @param datatype TODO javadoc me please 162 * @see #getDatatype() 163 */ 164 public BasicValidateElement(String datatype) { 165 super(datatype); 166 } 167 168 @Override 169 protected void appendXML(XmlStringBuilder buf) { 170 buf.emptyElement(METHOD); 171 } 172 173 @Override 174 public void checkConsistency(FormField.Builder<?, ?> formField) { 175 checkListRangeConsistency(formField); 176 if (formField.getType() != null) { 177 switch (formField.getType()) { 178 case hidden: 179 case jid_multi: 180 case jid_single: 181 throw new ValidationConsistencyException(String.format( 182 "Field type '%1$s' is not consistent with validation method '%2$s'.", 183 formField.getType(), BasicValidateElement.METHOD)); 184 default: 185 break; 186 } 187 } 188 } 189 190 } 191 192 /** 193 * For "list-single" or "list-multi", indicates that the user may enter a custom value (matching the datatype 194 * constraints) or choose from the predefined values. 195 * 196 * @see ValidateElement 197 */ 198 public static class OpenValidateElement extends ValidateElement { 199 200 public static final String METHOD = "open"; 201 202 /** 203 * Open validate element constructor. 204 * @param datatype TODO javadoc me please 205 * @see #getDatatype() 206 */ 207 public OpenValidateElement(String datatype) { 208 super(datatype); 209 } 210 211 @Override 212 protected void appendXML(XmlStringBuilder buf) { 213 buf.emptyElement(METHOD); 214 } 215 216 @Override 217 public void checkConsistency(FormField.Builder<?, ?> formField) { 218 checkListRangeConsistency(formField); 219 if (formField.getType() != null) { 220 switch (formField.getType()) { 221 case hidden: 222 throw new ValidationConsistencyException(String.format( 223 "Field type '%1$s' is not consistent with validation method '%2$s'.", 224 formField.getType(), OpenValidateElement.METHOD)); 225 default: 226 break; 227 } 228 } 229 } 230 231 } 232 233 /** 234 * Indicate that the value should fall within a certain range. 235 * 236 * @see ValidateElement 237 */ 238 public static class RangeValidateElement extends ValidateElement { 239 240 public static final String METHOD = "range"; 241 private final String min; 242 private final String max; 243 244 /** 245 * Range validate element constructor. 246 * @param datatype TODO javadoc me please 247 * @param min the minimum allowable value. This attribute is OPTIONAL. The value depends on the datatype in use. 248 * @param max the maximum allowable value. This attribute is OPTIONAL. The value depends on the datatype in use. 249 * @see #getDatatype() 250 * 251 */ 252 public RangeValidateElement(String datatype, String min, String max) { 253 super(datatype); 254 this.min = min; 255 this.max = max; 256 } 257 258 @Override 259 protected void appendXML(XmlStringBuilder buf) { 260 buf.halfOpenElement(METHOD); 261 buf.optAttribute("min", getMin()); 262 buf.optAttribute("max", getMax()); 263 buf.closeEmptyElement(); 264 } 265 266 /** 267 * The 'min' attribute specifies the minimum allowable value. 268 * 269 * @return the minimum allowable value. This attribute is OPTIONAL. The value depends on the datatype in use. 270 */ 271 public String getMin() { 272 return min; 273 } 274 275 /** 276 * The 'max' attribute specifies the maximum allowable value. 277 * 278 * @return the maximum allowable value. This attribute is OPTIONAL. The value depends on the datatype in use. 279 */ 280 public String getMax() { 281 return max; 282 } 283 284 @Override 285 public void checkConsistency(FormField.Builder<?, ?> formField) { 286 checkNonMultiConsistency(formField, METHOD); 287 if (getDatatype().equals(ValidateElement.DATATYPE_XS_STRING)) { 288 throw new ValidationConsistencyException(String.format( 289 "Field data type '%1$s' is not consistent with validation method '%2$s'.", 290 getDatatype(), RangeValidateElement.METHOD)); 291 } 292 } 293 294 @Override 295 public void validate(FormField formField) { 296 AbstractSingleStringValueFormField singleValueFormField = formField.ifPossibleAs(AbstractSingleStringValueFormField.class); 297 if (singleValueFormField == null) { 298 // We currently only implement validation for single value fields. 299 return; 300 } 301 String valueString = singleValueFormField.getValue(); 302 303 switch (getDatatype()) { 304 case "xs:int": 305 case "xs:integer": 306 BigInteger value = new BigInteger(valueString); 307 308 String minString = getMin(); 309 if (minString != null) { 310 BigInteger min = new BigInteger(minString); 311 if (value.compareTo(min) < 0) { 312 throw new IllegalArgumentException("The provided value " + valueString + " is lower than the allowed minimum of " + minString); 313 } 314 } 315 316 String maxString = getMax(); 317 if (maxString != null) { 318 BigInteger max = new BigInteger(maxString); 319 if (value.compareTo(max) > 0) { 320 throw new IllegalArgumentException("The provided value " + valueString + " is higher than the allowed maximum of " + maxString); 321 } 322 } 323 break; 324 } 325 } 326 } 327 328 /** 329 * Indicates that the value should be restricted to a regular expression. The regular expression must be that 330 * defined for <a href="http://www.xmpp.org/extensions/xep-0122.html#nt-idp1501344"> POSIX extended regular 331 * expressions </a> including support for <a 332 * href="http://www.xmpp.org/extensions/xep-0122.html#nt-idp1502496">Unicode</a>. 333 * 334 * @see ValidateElement 335 */ 336 public static class RegexValidateElement extends ValidateElement { 337 338 public static final String METHOD = "regex"; 339 private final String regex; 340 341 /** 342 * Regex validate element. 343 * @param datatype TODO javadoc me please 344 * @param regex TODO javadoc me please 345 * @see #getDatatype() 346 */ 347 public RegexValidateElement(String datatype, String regex) { 348 super(datatype); 349 this.regex = regex; 350 } 351 352 /** 353 * the expression is that defined for POSIX extended regular expressions, including support for Unicode. 354 * 355 * @return the regex 356 */ 357 public String getRegex() { 358 return regex; 359 } 360 361 @Override 362 protected void appendXML(XmlStringBuilder buf) { 363 buf.element("regex", getRegex()); 364 } 365 366 @Override 367 public void checkConsistency(FormField.Builder<?, ?> formField) { 368 checkNonMultiConsistency(formField, METHOD); 369 } 370 371 } 372 373 /** 374 * This element indicates for "list-multi", that a minimum and maximum number of options should be selected and/or 375 * entered. 376 */ 377 public static class ListRange implements XmlElement { 378 379 public static final String ELEMENT = "list-range"; 380 private final UInt32 min; 381 private final UInt32 max; 382 383 public ListRange(Long min, Long max) { 384 this(min != null ? UInt32.from(min) : null, max != null ? UInt32.from(max) : null); 385 } 386 387 /** 388 * The 'max' attribute specifies the maximum allowable number of selected/entered values. The 'min' attribute 389 * specifies the minimum allowable number of selected/entered values. Both attributes are optional, but at 390 * least one must bet set, and the value must be within the range of a unsigned 32-bit integer. 391 * 392 * @param min TODO javadoc me please 393 * @param max TODO javadoc me please 394 */ 395 public ListRange(UInt32 min, UInt32 max) { 396 if (max == null && min == null) { 397 throw new IllegalArgumentException("Either min or max must be given"); 398 } 399 this.min = min; 400 this.max = max; 401 } 402 403 @Override 404 public XmlStringBuilder toXML(XmlEnvironment enclosingXmlEnvironment) { 405 XmlStringBuilder buf = new XmlStringBuilder(this, enclosingXmlEnvironment); 406 buf.optAttributeCs("min", getMin()); 407 buf.optAttributeCs("max", getMax()); 408 buf.closeEmptyElement(); 409 return buf; 410 } 411 412 @Override 413 public String getElementName() { 414 return ELEMENT; 415 } 416 417 /** 418 * The minimum allowable number of selected/entered values. 419 * 420 * @return a positive integer, can be null 421 */ 422 public UInt32 getMin() { 423 return min; 424 } 425 426 /** 427 * The maximum allowable number of selected/entered values. 428 * 429 * @return a positive integer, can be null 430 */ 431 public UInt32 getMax() { 432 return max; 433 } 434 435 @Override 436 public String getNamespace() { 437 return NAMESPACE; 438 } 439 440 } 441 442 /** 443 * The >list-range/< element SHOULD be included only when the <field/> is of type "list-multi" and SHOULD be ignored 444 * otherwise. 445 * 446 * @param formField TODO javadoc me please 447 */ 448 protected void checkListRangeConsistency(FormField.Builder<?, ?> formField) { 449 ListRange listRange = getListRange(); 450 if (listRange == null) { 451 return; 452 } 453 454 Object max = listRange.getMax(); 455 Object min = listRange.getMin(); 456 if ((max != null || min != null) && formField.getType() != FormField.Type.list_multi) { 457 throw new ValidationConsistencyException( 458 "Field type is not of type 'list-multi' while a 'list-range' is defined."); 459 } 460 } 461 462 /** 463 * Check that the field being build is not of type multi (or hidden). 464 * 465 * @param formField TODO javadoc me please 466 * @param method TODO javadoc me please 467 */ 468 protected void checkNonMultiConsistency(FormField.Builder<?, ?> formField, String method) { 469 checkListRangeConsistency(formField); 470 if (formField.getType() != null) { 471 switch (formField.getType()) { 472 case hidden: 473 case jid_multi: 474 case list_multi: 475 case text_multi: 476 throw new ValidationConsistencyException(String.format( 477 "Field type '%1$s' is not consistent with validation method '%2$s'.", 478 formField.getType(), method)); 479 default: 480 break; 481 } 482 } 483 } 484} 485