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     * @param listRange the listRange to set
102     */
103    public void setListRange(ListRange listRange) {
104        this.listRange = listRange;
105    }
106
107    /**
108     * @return the listRange
109     */
110    public ListRange getListRange() {
111        return listRange;
112    }
113
114    /**
115     * Check if this element is consistent according to the business rules in XEP=0122
116     * 
117     * @param formField
118     */
119    public abstract void checkConsistency(FormField formField);
120
121    /**
122     * Validation only against the datatype itself. Indicates that the value(s) should simply match the field type and
123     * datatype constraints.
124     * 
125     * @see ValidateElement
126     */
127    public static class BasicValidateElement extends ValidateElement {
128
129        public static final String METHOD = "basic";
130
131        /**
132         * @param dataType
133         * @see #getDatatype()
134         */
135        public BasicValidateElement(String dataType) {
136            super(dataType);
137        }
138
139        @Override
140        protected void appendXML(XmlStringBuilder buf) {
141            buf.emptyElement(METHOD);
142        }
143
144        public void checkConsistency(FormField formField) {
145            checkListRangeConsistency(formField);
146            if (formField.getType() != null) {
147                switch (formField.getType()) {
148                case hidden:
149                case jid_multi:
150                case jid_single:
151                    throw new ValidationConsistencyException(String.format(
152                                    "Field type '%1$s' is not consistent with validation method '%2$s'.",
153                                    formField.getType(), BasicValidateElement.METHOD));
154                default:
155                    break;
156                }
157            }
158        }
159
160    }
161
162    /**
163     * For "list-single" or "list-multi", indicates that the user may enter a custom value (matching the datatype
164     * constraints) or choose from the predefined values.
165     * 
166     * @see ValidateElement
167     */
168    public static class OpenValidateElement extends ValidateElement {
169
170        public static final String METHOD = "open";
171
172        /**
173         * @param dataType
174         * @see #getDatatype()
175         */
176        public OpenValidateElement(String dataType) {
177            super(dataType);
178        }
179
180        @Override
181        protected void appendXML(XmlStringBuilder buf) {
182            buf.emptyElement(METHOD);
183        }
184
185        public void checkConsistency(FormField formField) {
186            checkListRangeConsistency(formField);
187            if (formField.getType() != null) {
188                switch (formField.getType()) {
189                case hidden:
190                    throw new ValidationConsistencyException(String.format(
191                                    "Field type '%1$s' is not consistent with validation method '%2$s'.",
192                                    formField.getType(), OpenValidateElement.METHOD));
193                default:
194                    break;
195                }
196            }
197        }
198
199    }
200
201    /**
202     * Indicate that the value should fall within a certain range.
203     * 
204     * @see ValidateElement
205     */
206    public static class RangeValidateElement extends ValidateElement {
207
208        public static final String METHOD = "range";
209        private final String min;
210        private final String max;
211
212        /**
213         * @param dataType
214         * @param min the minimum allowable value. This attribute is OPTIONAL. The value depends on the datatype in use.
215         * @param max the maximum allowable value. This attribute is OPTIONAL. The value depends on the datatype in use.
216         * @see #getDatatype()
217         * 
218         */
219        public RangeValidateElement(String dataType, String min, String max) {
220            super(dataType);
221            this.min = min;
222            this.max = max;
223        }
224
225        @Override
226        protected void appendXML(XmlStringBuilder buf) {
227            buf.halfOpenElement(METHOD);
228            buf.optAttribute("min", getMin());
229            buf.optAttribute("max", getMax());
230            buf.closeEmptyElement();
231        }
232
233        /**
234         * The 'min' attribute specifies the minimum allowable value.
235         * 
236         * @return the minimum allowable value. This attribute is OPTIONAL. The value depends on the datatype in use.
237         */
238        public String getMin() {
239            return min;
240        }
241
242        /**
243         * The 'max' attribute specifies the maximum allowable value.
244         * 
245         * @return the maximum allowable value. This attribute is OPTIONAL. The value depends on the datatype in use.
246         */
247        public String getMax() {
248            return max;
249        }
250
251        public void checkConsistency(FormField formField) {
252            checkNonMultiConsistency(formField, METHOD);
253            if (getDatatype().equals(ValidateElement.DATATYPE_XS_STRING)) {
254                throw new ValidationConsistencyException(String.format(
255                                "Field data type '%1$s' is not consistent with validation method '%2$s'.",
256                                getDatatype(), RangeValidateElement.METHOD));
257            }
258        }
259
260    }
261
262    /**
263     * Indicates that the value should be restricted to a regular expression. The regular expression must be that
264     * defined for <a href="http://www.xmpp.org/extensions/xep-0122.html#nt-idp1501344"> POSIX extended regular
265     * expressions </a> including support for <a
266     * href="http://www.xmpp.org/extensions/xep-0122.html#nt-idp1502496">Unicode</a>.
267     * 
268     * @see ValidateElement
269     */
270    public static class RegexValidateElement extends ValidateElement {
271
272        public static final String METHOD = "regex";
273        private final String regex;
274
275        /**
276         * @param dataType
277         * @param regex
278         * @see #getDatatype()
279         */
280        public RegexValidateElement(String dataType, String regex) {
281            super(dataType);
282            this.regex = regex;
283        }
284
285        /**
286         * the expression is that defined for POSIX extended regular expressions, including support for Unicode.
287         * 
288         * @return the regex
289         */
290        public String getRegex() {
291            return regex;
292        }
293
294        @Override
295        protected void appendXML(XmlStringBuilder buf) {
296            buf.element("regex", getRegex());
297        }
298
299        public void checkConsistency(FormField formField) {
300            checkNonMultiConsistency(formField, METHOD);
301        }
302
303    }
304
305    /**
306     * This element indicates for "list-multi", that a minimum and maximum number of options should be selected and/or
307     * entered.
308     */
309    public static class ListRange implements NamedElement {
310
311        public static final String ELEMENT = "list-range";
312        private final Long min;
313        private final Long max;
314
315        /**
316         * The 'max' attribute specifies the maximum allowable number of selected/entered values. The 'min' attribute
317         * specifies the minimum allowable number of selected/entered values. Both attributes are optional, but at
318         * least one must bet set, and the value must be within the range of a unsigned 32-bit integer.
319         * 
320         * @param min
321         * @param max
322         */
323        public ListRange(Long min, Long max) {
324            if (min != null) {
325                NumberUtil.checkIfInUInt32Range(min);
326            }
327            if (max != null) {
328                NumberUtil.checkIfInUInt32Range(max);
329            }
330            if (max == null && min == null) {
331                throw new IllegalArgumentException("Either min or max must be given");
332            }
333            this.min = min;
334            this.max = max;
335        }
336
337        public XmlStringBuilder toXML() {
338            XmlStringBuilder buf = new XmlStringBuilder(this);
339            buf.optLongAttribute("min", getMin());
340            buf.optLongAttribute("max", getMax());
341            buf.closeEmptyElement();
342            return buf;
343        }
344
345        @Override
346        public String getElementName() {
347            return ELEMENT;
348        }
349
350        /**
351         * The minimum allowable number of selected/entered values.
352         * 
353         * @return a positive integer, can be null
354         */
355        public Long getMin() {
356            return min;
357        }
358
359        /**
360         * The maximum allowable number of selected/entered values.
361         * 
362         * @return a positive integer, can be null
363         */
364        public Long getMax() {
365            return max;
366        }
367
368    }
369
370    /**
371     * The <list-range/> element SHOULD be included only when the <field/> is of type "list-multi" and SHOULD be ignored
372     * otherwise.
373     * 
374     * @param formField
375     */
376    protected void checkListRangeConsistency(FormField formField) {
377        ListRange listRange = getListRange();
378        if (listRange == null) {
379            return;
380        }
381
382        Long max = listRange.getMax();
383        Long min = listRange.getMin();
384        if ((max != null || min != null) && formField.getType() != FormField.Type.list_multi) {
385            throw new ValidationConsistencyException(
386                            "Field type is not of type 'list-multi' while a 'list-range' is defined.");
387        }
388    }
389
390    /**
391     * @param formField
392     * @param method
393     */
394    protected void checkNonMultiConsistency(FormField formField, String method) {
395        checkListRangeConsistency(formField);
396        if (formField.getType() != null) {
397            switch (formField.getType()) {
398            case hidden:
399            case jid_multi:
400            case list_multi:
401            case text_multi:
402                throw new ValidationConsistencyException(String.format(
403                                "Field type '%1$s' is not consistent with validation method '%2$s'.",
404                                formField.getType(), method));
405            default:
406                break;
407            }
408        }
409    }
410}
411