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