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