001/**
002 *
003 * Copyright 2014 Anno van Vliet
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 org.jivesoftware.smack.packet.ExtensionElement;
020import org.jivesoftware.smack.packet.NamedElement;
021import org.jivesoftware.smack.util.NumberUtil;
022import org.jivesoftware.smack.util.StringUtils;
023import org.jivesoftware.smack.util.XmlStringBuilder;
024
025import org.jivesoftware.smackx.xdata.FormField;
026import org.jivesoftware.smackx.xdata.packet.DataForm;
027import org.jivesoftware.smackx.xdatavalidation.ValidationConsistencyException;
028
029/**
030 * DataValidation Extension according to XEP-0122: Data Forms Validation. This specification defines a
031 * backwards-compatible extension to the XMPP Data Forms protocol that enables applications to specify additional
032 * validation guidelines related to a {@link FormField} in a {@link DataForm}, such as validation of standard XML
033 * datatypes, application-specific datatypes, value ranges, and regular expressions.
034 *
035 * @author Anno van Vliet
036 */
037public abstract class ValidateElement implements ExtensionElement {
038
039    public static final String DATATYPE_XS_STRING = "xs:string";
040    public static final String ELEMENT = "validate";
041    public static final String NAMESPACE = "http://jabber.org/protocol/xdata-validate";
042
043    private final String datatype;
044
045    private ListRange listRange;
046
047    /**
048     * The 'datatype' attribute specifies the datatype. This attribute is OPTIONAL, and when not specified, defaults to
049     * "xs:string".
050     *
051     * @param datatype the data type of any value contained within the {@link FormField} element.
052     */
053    private ValidateElement(String datatype) {
054        this.datatype = StringUtils.isNotEmpty(datatype) ? datatype : null;
055    }
056
057    /**
058     * Specifies the data type of any value contained within the {@link FormField} element. It MUST meet one of the
059     * following conditions:
060     * <ul>
061     * <li>Start with "xs:", and be one of the "built-in" datatypes defined in XML Schema Part 2 <a
062     * href="http://www.xmpp.org/extensions/xep-0122.html#nt-idp1476016">[2]</a></li>
063     * <li>Start with a prefix registered with the XMPP Registrar <a
064     * href="http://www.xmpp.org/extensions/xep-0122.html#nt-idp1478544">[3]</a></li>
065     * <li>Start with "x:", and specify a user-defined datatype <a
066     * href="http://www.xmpp.org/extensions/xep-0122.html#nt-idp1477360">[4]</a></li>
067     * </ul>
068     *
069     * @return the datatype
070     */
071    public String getDatatype() {
072        return datatype != null ? datatype : DATATYPE_XS_STRING;
073    }
074
075    @Override
076    public String getElementName() {
077        return ELEMENT;
078    }
079
080    @Override
081    public String getNamespace() {
082        return NAMESPACE;
083    }
084
085    @Override
086    public XmlStringBuilder toXML(String enclosingNamespace) {
087        XmlStringBuilder buf = new XmlStringBuilder(this);
088        buf.optAttribute("datatype", datatype);
089        buf.rightAngleBracket();
090        appendXML(buf);
091        buf.optAppend(getListRange());
092        buf.closeElement(this);
093        return buf;
094    }
095
096    /**
097     * @param buf
098     */
099    protected abstract void appendXML(XmlStringBuilder buf);
100
101    /**
102     * Set list range.
103     * @param listRange the listRange to set
104     */
105    public void setListRange(ListRange listRange) {
106        this.listRange = listRange;
107    }
108
109    /**
110     * Get list range.
111     * @return the listRange
112     */
113    public ListRange getListRange() {
114        return listRange;
115    }
116
117    /**
118     * Check if this element is consistent according to the business rules in XEP=0122.
119     *
120     * @param formField
121     */
122    public abstract void checkConsistency(FormField formField);
123
124    /**
125     * Validation only against the datatype itself. Indicates that the value(s) should simply match the field type and
126     * datatype constraints.
127     *
128     * @see ValidateElement
129     */
130    public static class BasicValidateElement extends ValidateElement {
131
132        public static final String METHOD = "basic";
133
134        /**
135         * Basic validate element constructor.
136         * @param dataType
137         * @see #getDatatype()
138         */
139        public BasicValidateElement(String dataType) {
140            super(dataType);
141        }
142
143        @Override
144        protected void appendXML(XmlStringBuilder buf) {
145            buf.emptyElement(METHOD);
146        }
147
148        @Override
149        public void checkConsistency(FormField formField) {
150            checkListRangeConsistency(formField);
151            if (formField.getType() != null) {
152                switch (formField.getType()) {
153                case hidden:
154                case jid_multi:
155                case jid_single:
156                    throw new ValidationConsistencyException(String.format(
157                                    "Field type '%1$s' is not consistent with validation method '%2$s'.",
158                                    formField.getType(), BasicValidateElement.METHOD));
159                default:
160                    break;
161                }
162            }
163        }
164
165    }
166
167    /**
168     * For "list-single" or "list-multi", indicates that the user may enter a custom value (matching the datatype
169     * constraints) or choose from the predefined values.
170     *
171     * @see ValidateElement
172     */
173    public static class OpenValidateElement extends ValidateElement {
174
175        public static final String METHOD = "open";
176
177        /**
178         * Open validate element constructor.
179         * @param dataType
180         * @see #getDatatype()
181         */
182        public OpenValidateElement(String dataType) {
183            super(dataType);
184        }
185
186        @Override
187        protected void appendXML(XmlStringBuilder buf) {
188            buf.emptyElement(METHOD);
189        }
190
191        @Override
192        public void checkConsistency(FormField formField) {
193            checkListRangeConsistency(formField);
194            if (formField.getType() != null) {
195                switch (formField.getType()) {
196                case hidden:
197                    throw new ValidationConsistencyException(String.format(
198                                    "Field type '%1$s' is not consistent with validation method '%2$s'.",
199                                    formField.getType(), OpenValidateElement.METHOD));
200                default:
201                    break;
202                }
203            }
204        }
205
206    }
207
208    /**
209     * Indicate that the value should fall within a certain range.
210     *
211     * @see ValidateElement
212     */
213    public static class RangeValidateElement extends ValidateElement {
214
215        public static final String METHOD = "range";
216        private final String min;
217        private final String max;
218
219        /**
220         * Range validate element constructor.
221         * @param dataType
222         * @param min the minimum allowable value. This attribute is OPTIONAL. The value depends on the datatype in use.
223         * @param max the maximum allowable value. This attribute is OPTIONAL. The value depends on the datatype in use.
224         * @see #getDatatype()
225         *
226         */
227        public RangeValidateElement(String dataType, String min, String max) {
228            super(dataType);
229            this.min = min;
230            this.max = max;
231        }
232
233        @Override
234        protected void appendXML(XmlStringBuilder buf) {
235            buf.halfOpenElement(METHOD);
236            buf.optAttribute("min", getMin());
237            buf.optAttribute("max", getMax());
238            buf.closeEmptyElement();
239        }
240
241        /**
242         * The 'min' attribute specifies the minimum allowable value.
243         *
244         * @return the minimum allowable value. This attribute is OPTIONAL. The value depends on the datatype in use.
245         */
246        public String getMin() {
247            return min;
248        }
249
250        /**
251         * The 'max' attribute specifies the maximum allowable value.
252         *
253         * @return the maximum allowable value. This attribute is OPTIONAL. The value depends on the datatype in use.
254         */
255        public String getMax() {
256            return max;
257        }
258
259        @Override
260        public void checkConsistency(FormField formField) {
261            checkNonMultiConsistency(formField, METHOD);
262            if (getDatatype().equals(ValidateElement.DATATYPE_XS_STRING)) {
263                throw new ValidationConsistencyException(String.format(
264                                "Field data type '%1$s' is not consistent with validation method '%2$s'.",
265                                getDatatype(), RangeValidateElement.METHOD));
266            }
267        }
268
269    }
270
271    /**
272     * Indicates that the value should be restricted to a regular expression. The regular expression must be that
273     * defined for <a href="http://www.xmpp.org/extensions/xep-0122.html#nt-idp1501344"> POSIX extended regular
274     * expressions </a> including support for <a
275     * href="http://www.xmpp.org/extensions/xep-0122.html#nt-idp1502496">Unicode</a>.
276     *
277     * @see ValidateElement
278     */
279    public static class RegexValidateElement extends ValidateElement {
280
281        public static final String METHOD = "regex";
282        private final String regex;
283
284        /**
285         * Regex validate element.
286         * @param dataType
287         * @param regex
288         * @see #getDatatype()
289         */
290        public RegexValidateElement(String dataType, String regex) {
291            super(dataType);
292            this.regex = regex;
293        }
294
295        /**
296         * the expression is that defined for POSIX extended regular expressions, including support for Unicode.
297         *
298         * @return the regex
299         */
300        public String getRegex() {
301            return regex;
302        }
303
304        @Override
305        protected void appendXML(XmlStringBuilder buf) {
306            buf.element("regex", getRegex());
307        }
308
309        @Override
310        public void checkConsistency(FormField formField) {
311            checkNonMultiConsistency(formField, METHOD);
312        }
313
314    }
315
316    /**
317     * This element indicates for "list-multi", that a minimum and maximum number of options should be selected and/or
318     * entered.
319     */
320    public static class ListRange implements NamedElement {
321
322        public static final String ELEMENT = "list-range";
323        private final Long min;
324        private final Long max;
325
326        /**
327         * The 'max' attribute specifies the maximum allowable number of selected/entered values. The 'min' attribute
328         * specifies the minimum allowable number of selected/entered values. Both attributes are optional, but at
329         * least one must bet set, and the value must be within the range of a unsigned 32-bit integer.
330         *
331         * @param min
332         * @param max
333         */
334        public ListRange(Long min, Long max) {
335            if (min != null) {
336                NumberUtil.checkIfInUInt32Range(min);
337            }
338            if (max != null) {
339                NumberUtil.checkIfInUInt32Range(max);
340            }
341            if (max == null && min == null) {
342                throw new IllegalArgumentException("Either min or max must be given");
343            }
344            this.min = min;
345            this.max = max;
346        }
347
348        @Override
349        public XmlStringBuilder toXML(String enclosingNamespace) {
350            XmlStringBuilder buf = new XmlStringBuilder(this);
351            buf.optLongAttribute("min", getMin());
352            buf.optLongAttribute("max", getMax());
353            buf.closeEmptyElement();
354            return buf;
355        }
356
357        @Override
358        public String getElementName() {
359            return ELEMENT;
360        }
361
362        /**
363         * The minimum allowable number of selected/entered values.
364         *
365         * @return a positive integer, can be null
366         */
367        public Long getMin() {
368            return min;
369        }
370
371        /**
372         * The maximum allowable number of selected/entered values.
373         *
374         * @return a positive integer, can be null
375         */
376        public Long getMax() {
377            return max;
378        }
379
380    }
381
382    /**
383     * The &gt;list-range/&lt; element SHOULD be included only when the &lt;field/&gt; is of type "list-multi" and SHOULD be ignored
384     * otherwise.
385     *
386     * @param formField
387     */
388    protected void checkListRangeConsistency(FormField formField) {
389        ListRange listRange = getListRange();
390        if (listRange == null) {
391            return;
392        }
393
394        Long max = listRange.getMax();
395        Long min = listRange.getMin();
396        if ((max != null || min != null) && formField.getType() != FormField.Type.list_multi) {
397            throw new ValidationConsistencyException(
398                            "Field type is not of type 'list-multi' while a 'list-range' is defined.");
399        }
400    }
401
402    /**
403     * @param formField
404     * @param method
405     */
406    protected void checkNonMultiConsistency(FormField formField, String method) {
407        checkListRangeConsistency(formField);
408        if (formField.getType() != null) {
409            switch (formField.getType()) {
410            case hidden:
411            case jid_multi:
412            case list_multi:
413            case text_multi:
414                throw new ValidationConsistencyException(String.format(
415                                "Field type '%1$s' is not consistent with validation method '%2$s'.",
416                                formField.getType(), method));
417            default:
418                break;
419            }
420        }
421    }
422}
423