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 &gt;list-range/&lt; element SHOULD be included only when the &lt;field/&gt; 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