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.FullyQualifiedElement; 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 * @param buf TODO javadoc me please 116 */ 117 protected abstract void appendXML(XmlStringBuilder buf); 118 119 /** 120 * Set list range. 121 * @param listRange the listRange to set 122 */ 123 public void setListRange(ListRange listRange) { 124 this.listRange = listRange; 125 } 126 127 /** 128 * Get list range. 129 * @return the listRange 130 */ 131 public ListRange getListRange() { 132 return listRange; 133 } 134 135 /** 136 * Check if this element is consistent according to the business rules in XEP-0122. 137 * 138 * @param formFieldBuilder the builder used to construct the form field. 139 */ 140 @Override 141 public abstract void checkConsistency(FormField.Builder<?, ?> formFieldBuilder); 142 143 public static ValidateElement from(FormField formField) { 144 return (ValidateElement) formField.getFormFieldChildElement(QNAME); 145 } 146 147 /** 148 * Validation only against the datatype itself. Indicates that the value(s) should simply match the field type and 149 * datatype constraints. 150 * 151 * @see ValidateElement 152 */ 153 public static class BasicValidateElement extends ValidateElement { 154 155 public static final String METHOD = "basic"; 156 157 /** 158 * Basic validate element constructor. 159 * @param datatype TODO javadoc me please 160 * @see #getDatatype() 161 */ 162 public BasicValidateElement(String datatype) { 163 super(datatype); 164 } 165 166 @Override 167 protected void appendXML(XmlStringBuilder buf) { 168 buf.emptyElement(METHOD); 169 } 170 171 @Override 172 public void checkConsistency(FormField.Builder<?, ?> formField) { 173 checkListRangeConsistency(formField); 174 if (formField.getType() != null) { 175 switch (formField.getType()) { 176 case hidden: 177 case jid_multi: 178 case jid_single: 179 throw new ValidationConsistencyException(String.format( 180 "Field type '%1$s' is not consistent with validation method '%2$s'.", 181 formField.getType(), BasicValidateElement.METHOD)); 182 default: 183 break; 184 } 185 } 186 } 187 188 } 189 190 /** 191 * For "list-single" or "list-multi", indicates that the user may enter a custom value (matching the datatype 192 * constraints) or choose from the predefined values. 193 * 194 * @see ValidateElement 195 */ 196 public static class OpenValidateElement extends ValidateElement { 197 198 public static final String METHOD = "open"; 199 200 /** 201 * Open validate element constructor. 202 * @param datatype TODO javadoc me please 203 * @see #getDatatype() 204 */ 205 public OpenValidateElement(String datatype) { 206 super(datatype); 207 } 208 209 @Override 210 protected void appendXML(XmlStringBuilder buf) { 211 buf.emptyElement(METHOD); 212 } 213 214 @Override 215 public void checkConsistency(FormField.Builder<?, ?> formField) { 216 checkListRangeConsistency(formField); 217 if (formField.getType() != null) { 218 switch (formField.getType()) { 219 case hidden: 220 throw new ValidationConsistencyException(String.format( 221 "Field type '%1$s' is not consistent with validation method '%2$s'.", 222 formField.getType(), OpenValidateElement.METHOD)); 223 default: 224 break; 225 } 226 } 227 } 228 229 } 230 231 /** 232 * Indicate that the value should fall within a certain range. 233 * 234 * @see ValidateElement 235 */ 236 public static class RangeValidateElement extends ValidateElement { 237 238 public static final String METHOD = "range"; 239 private final String min; 240 private final String max; 241 242 /** 243 * Range validate element constructor. 244 * @param datatype TODO javadoc me please 245 * @param min the minimum allowable value. This attribute is OPTIONAL. The value depends on the datatype in use. 246 * @param max the maximum allowable value. This attribute is OPTIONAL. The value depends on the datatype in use. 247 * @see #getDatatype() 248 * 249 */ 250 public RangeValidateElement(String datatype, String min, String max) { 251 super(datatype); 252 this.min = min; 253 this.max = max; 254 } 255 256 @Override 257 protected void appendXML(XmlStringBuilder buf) { 258 buf.halfOpenElement(METHOD); 259 buf.optAttribute("min", getMin()); 260 buf.optAttribute("max", getMax()); 261 buf.closeEmptyElement(); 262 } 263 264 /** 265 * The 'min' attribute specifies the minimum allowable value. 266 * 267 * @return the minimum allowable value. This attribute is OPTIONAL. The value depends on the datatype in use. 268 */ 269 public String getMin() { 270 return min; 271 } 272 273 /** 274 * The 'max' attribute specifies the maximum allowable value. 275 * 276 * @return the maximum allowable value. This attribute is OPTIONAL. The value depends on the datatype in use. 277 */ 278 public String getMax() { 279 return max; 280 } 281 282 @Override 283 public void checkConsistency(FormField.Builder<?, ?> formField) { 284 checkNonMultiConsistency(formField, METHOD); 285 if (getDatatype().equals(ValidateElement.DATATYPE_XS_STRING)) { 286 throw new ValidationConsistencyException(String.format( 287 "Field data type '%1$s' is not consistent with validation method '%2$s'.", 288 getDatatype(), RangeValidateElement.METHOD)); 289 } 290 } 291 292 @Override 293 public void validate(FormField formField) { 294 AbstractSingleStringValueFormField singleValueFormField = formField.ifPossibleAs(AbstractSingleStringValueFormField.class); 295 if (singleValueFormField == null) { 296 // We currently only implement validation for single value fields. 297 return; 298 } 299 String valueString = singleValueFormField.getValue(); 300 301 switch (getDatatype()) { 302 case "xs:int": 303 case "xs:integer": 304 BigInteger value = new BigInteger(valueString); 305 306 String minString = getMin(); 307 if (minString != null) { 308 BigInteger min = new BigInteger(minString); 309 if (value.compareTo(min) < 0) { 310 throw new IllegalArgumentException("The provided value " + valueString + " is lower than the allowed minimum of " + minString); 311 } 312 } 313 314 String maxString = getMax(); 315 if (maxString != null) { 316 BigInteger max = new BigInteger(maxString); 317 if (value.compareTo(max) > 0) { 318 throw new IllegalArgumentException("The provided value " + valueString + " is higher than the allowed maximum of " + maxString); 319 } 320 } 321 break; 322 } 323 } 324 } 325 326 /** 327 * Indicates that the value should be restricted to a regular expression. The regular expression must be that 328 * defined for <a href="http://www.xmpp.org/extensions/xep-0122.html#nt-idp1501344"> POSIX extended regular 329 * expressions </a> including support for <a 330 * href="http://www.xmpp.org/extensions/xep-0122.html#nt-idp1502496">Unicode</a>. 331 * 332 * @see ValidateElement 333 */ 334 public static class RegexValidateElement extends ValidateElement { 335 336 public static final String METHOD = "regex"; 337 private final String regex; 338 339 /** 340 * Regex validate element. 341 * @param datatype TODO javadoc me please 342 * @param regex TODO javadoc me please 343 * @see #getDatatype() 344 */ 345 public RegexValidateElement(String datatype, String regex) { 346 super(datatype); 347 this.regex = regex; 348 } 349 350 /** 351 * the expression is that defined for POSIX extended regular expressions, including support for Unicode. 352 * 353 * @return the regex 354 */ 355 public String getRegex() { 356 return regex; 357 } 358 359 @Override 360 protected void appendXML(XmlStringBuilder buf) { 361 buf.element("regex", getRegex()); 362 } 363 364 @Override 365 public void checkConsistency(FormField.Builder<?, ?> formField) { 366 checkNonMultiConsistency(formField, METHOD); 367 } 368 369 } 370 371 /** 372 * This element indicates for "list-multi", that a minimum and maximum number of options should be selected and/or 373 * entered. 374 */ 375 public static class ListRange implements FullyQualifiedElement { 376 377 public static final String ELEMENT = "list-range"; 378 private final UInt32 min; 379 private final UInt32 max; 380 381 public ListRange(Long min, Long max) { 382 this(min != null ? UInt32.from(min) : null, max != null ? UInt32.from(max) : null); 383 } 384 385 /** 386 * The 'max' attribute specifies the maximum allowable number of selected/entered values. The 'min' attribute 387 * specifies the minimum allowable number of selected/entered values. Both attributes are optional, but at 388 * least one must bet set, and the value must be within the range of a unsigned 32-bit integer. 389 * 390 * @param min TODO javadoc me please 391 * @param max TODO javadoc me please 392 */ 393 public ListRange(UInt32 min, UInt32 max) { 394 if (max == null && min == null) { 395 throw new IllegalArgumentException("Either min or max must be given"); 396 } 397 this.min = min; 398 this.max = max; 399 } 400 401 @Override 402 public XmlStringBuilder toXML(XmlEnvironment enclosingXmlEnvironment) { 403 XmlStringBuilder buf = new XmlStringBuilder(this, enclosingXmlEnvironment); 404 buf.optAttributeCs("min", getMin()); 405 buf.optAttributeCs("max", getMax()); 406 buf.closeEmptyElement(); 407 return buf; 408 } 409 410 @Override 411 public String getElementName() { 412 return ELEMENT; 413 } 414 415 /** 416 * The minimum allowable number of selected/entered values. 417 * 418 * @return a positive integer, can be null 419 */ 420 public UInt32 getMin() { 421 return min; 422 } 423 424 /** 425 * The maximum allowable number of selected/entered values. 426 * 427 * @return a positive integer, can be null 428 */ 429 public UInt32 getMax() { 430 return max; 431 } 432 433 @Override 434 public String getNamespace() { 435 return NAMESPACE; 436 } 437 438 } 439 440 /** 441 * The >list-range/< element SHOULD be included only when the <field/> is of type "list-multi" and SHOULD be ignored 442 * otherwise. 443 * 444 * @param formField TODO javadoc me please 445 */ 446 protected void checkListRangeConsistency(FormField.Builder<?, ?> formField) { 447 ListRange listRange = getListRange(); 448 if (listRange == null) { 449 return; 450 } 451 452 Object max = listRange.getMax(); 453 Object min = listRange.getMin(); 454 if ((max != null || min != null) && formField.getType() != FormField.Type.list_multi) { 455 throw new ValidationConsistencyException( 456 "Field type is not of type 'list-multi' while a 'list-range' is defined."); 457 } 458 } 459 460 /** 461 * @param formField TODO javadoc me please 462 * @param method TODO javadoc me please 463 */ 464 protected void checkNonMultiConsistency(FormField.Builder<?, ?> formField, String method) { 465 checkListRangeConsistency(formField); 466 if (formField.getType() != null) { 467 switch (formField.getType()) { 468 case hidden: 469 case jid_multi: 470 case list_multi: 471 case text_multi: 472 throw new ValidationConsistencyException(String.format( 473 "Field type '%1$s' is not consistent with validation method '%2$s'.", 474 formField.getType(), method)); 475 default: 476 break; 477 } 478 } 479 } 480} 481