1
0
Fork 0
mirror of https://codeberg.org/Mercury-IM/Smack synced 2025-12-09 22:51:07 +01:00

Support for XEP-0122: Data Forms Validation.

Data Forms Validation are a part of Data Fields and implemented as
extensions, added to a Datafield.

Data validation extensions are validated before adding to the message,
using the consistency rules as described in the XEP.
Fixes SMACK-621.

Minor modifications done by Florian Schmaus <flo@geekplace.eu>
This commit is contained in:
Anno van Vliet 2014-11-22 23:16:45 +01:00 committed by Florian Schmaus
parent 019b9dc5d4
commit b08dbc1dbc
13 changed files with 943 additions and 66 deletions

View file

@ -17,12 +17,13 @@
package org.jivesoftware.smackx.xdata;
import org.jivesoftware.smack.util.XmlStringBuilder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.jivesoftware.smack.util.XmlStringBuilder;
import org.jivesoftware.smackx.xdatavalidation.packet.ValidateElement;
/**
* Represents a field of a form. The field could be used to represent a question to complete,
* a completed question or a data returned from a search. The exact interpretation of the field
@ -124,6 +125,7 @@ public class FormField {
private Type type;
private final List<Option> options = new ArrayList<Option>();
private final List<String> values = new ArrayList<String>();
private ValidateElement validateElement;
/**
* Creates a new FormField with the variable name that uniquely identifies the field
@ -219,6 +221,13 @@ public class FormField {
return variable;
}
/**
* @return the validateElement
*/
public ValidateElement getValidateElement() {
return validateElement;
}
/**
* Sets a description that provides extra clarification about the question. This information
* could be presented to the user either in tool-tip, help button, or as a section of text
@ -251,6 +260,14 @@ public class FormField {
this.required = required;
}
/**
* @param validateElement the validateElement to set
*/
public void setValidateElement(ValidateElement validateElement) {
validateElement.checkConsistency(this);
this.validateElement = validateElement;
}
/**
* Sets an indicative of the format for the data to answer.
*
@ -325,6 +342,7 @@ public class FormField {
for (Option option : getOptions()) {
buf.append(option.toXML());
}
buf.optElement(validateElement);
buf.closeElement(ELEMENT);
return buf;
}

View file

@ -25,6 +25,8 @@ import org.jivesoftware.smackx.xdata.FormField;
import org.jivesoftware.smackx.xdata.packet.DataForm;
import org.jivesoftware.smackx.xdatalayout.packet.DataLayout;
import org.jivesoftware.smackx.xdatalayout.provider.DataLayoutProvider;
import org.jivesoftware.smackx.xdatavalidation.packet.ValidateElement;
import org.jivesoftware.smackx.xdatavalidation.provider.DataValidationProvider;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@ -102,6 +104,7 @@ public class DataFormProvider extends PacketExtensionProvider<DataForm> {
switch (eventType) {
case XmlPullParser.START_TAG:
String name = parser.getName();
String namespace = parser.getNamespace();
switch (name) {
case "desc":
formField.setDescription(parser.nextText());
@ -115,6 +118,12 @@ public class DataFormProvider extends PacketExtensionProvider<DataForm> {
case "option":
formField.addOption(parseOption(parser));
break;
// See XEP-122 Data Forms Validation
case ValidateElement.ELEMENT:
if (namespace.equals(ValidateElement.NAMESPACE)) {
formField.setValidateElement(DataValidationProvider.parse(parser));
}
break;
}
case XmlPullParser.END_TAG:
if (parser.getDepth() == initialDepth) {

View file

@ -0,0 +1,40 @@
/**
*
* Copyright 2014 Anno van Vliet
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.xdatavalidation;
import org.jivesoftware.smackx.xdatavalidation.packet.ValidateElement;
/**
* Exception thrown when {@link ValidateElement} is not consistent with the business rules in XEP=0122.
*
* @author Anno van Vliet
*
*/
public class ValidationConsistencyException extends IllegalArgumentException {
/**
*
*/
private static final long serialVersionUID = 1L;
/**
* @param message
*/
public ValidationConsistencyException(String message) {
super( message);
}
}

View file

@ -0,0 +1,37 @@
/**
*
* Copyright 2014 Anno van Vliet
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.xdatavalidation;
import org.jivesoftware.smack.ConnectionCreationListener;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPConnectionRegistry;
import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
import org.jivesoftware.smackx.xdatavalidation.packet.ValidateElement;
public class XDataValidationManager {
static {
XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() {
@Override
public void connectionCreated(XMPPConnection connection) {
ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection);
serviceDiscoveryManager.addFeature(ValidateElement.NAMESPACE);
}
});
}
}

View file

@ -0,0 +1,436 @@
/**
*
* Copyright 2014 Anno van Vliet
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.xdatavalidation.packet;
import org.jivesoftware.smack.packet.NamedElement;
import org.jivesoftware.smack.packet.PacketExtension;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smack.util.XmlStringBuilder;
import org.jivesoftware.smackx.xdata.FormField;
import org.jivesoftware.smackx.xdata.packet.DataForm;
import org.jivesoftware.smackx.xdatavalidation.ValidationConsistencyException;
/**
* DataValidation Extension according to XEP-0122: Data Forms Validation. This specification defines a
* backwards-compatible extension to the XMPP Data Forms protocol that enables applications to specify additional
* validation guidelines related to a {@link FormField} in a {@link DataForm}, such as validation of standard XML
* datatypes, application-specific datatypes, value ranges, and regular expressions.
*
* @author Anno van Vliet
*/
public abstract class ValidateElement implements PacketExtension {
public static final String DATATYPE_XS_STRING = "xs:string";
public static final String ELEMENT = "validate";
public static final String NAMESPACE = "http://jabber.org/protocol/xdata-validate";
private final String datatype;
private ListRange listRange;
/**
* The 'datatype' attribute specifies the datatype. This attribute is OPTIONAL, and when not specified, defaults to
* "xs:string".
*
* @param datatype the data type of any value contained within the {@link FormField} element.
*/
private ValidateElement(String datatype) {
this.datatype = StringUtils.isNotEmpty(datatype) ? datatype : null;
}
/**
* Specifies the data type of any value contained within the {@link FormField} element. It MUST meet one of the
* following conditions:
* <ul>
* <li>Start with "xs:", and be one of the "built-in" datatypes defined in XML Schema Part 2 <a
* href="http://www.xmpp.org/extensions/xep-0122.html#nt-idp1476016">[2]</a></li>
* <li>Start with a prefix registered with the XMPP Registrar <a
* href="http://www.xmpp.org/extensions/xep-0122.html#nt-idp1478544">[3]</a></li>
* <li>Start with "x:", and specify a user-defined datatype <a
* href="http://www.xmpp.org/extensions/xep-0122.html#nt-idp1477360">[4]</a></li>
* </ul>
*
* @return the datatype
*/
public String getDatatype() {
return datatype != null ? datatype : DATATYPE_XS_STRING;
}
@Override
public String getElementName() {
return ELEMENT;
}
@Override
public String getNamespace() {
return NAMESPACE;
}
@Override
public XmlStringBuilder toXML() {
XmlStringBuilder buf = new XmlStringBuilder(this);
buf.optAttribute("datatype", datatype);
buf.rightAngleBracket();
appendXML(buf);
buf.optAppend(getListRange());
buf.closeElement(this);
return buf;
}
/**
* @param buf
*/
protected abstract void appendXML(XmlStringBuilder buf);
/**
* @param listRange the listRange to set
*/
public void setListRange(ListRange listRange) {
this.listRange = listRange;
}
/**
* @return the listRange
*/
public ListRange getListRange() {
return listRange;
}
/**
* Check if this element is consistent according to the business rules in XEP=0122
*
* @param formField
*/
public abstract void checkConsistency(FormField formField);
/**
*
* This defines the empty validate element that does only specify a 'datatype' attribute.
*
*/
public static class EmptyValidateElement extends ValidateElement {
/**
* @param dataType
* @see #getDatatype()
*/
public EmptyValidateElement(String dataType) {
super(dataType);
}
@Override
protected void appendXML(XmlStringBuilder buf) {
// The empty validate element does not contain any further elements or text, it is empty.
}
@Override
public void checkConsistency(FormField formField) {
// Since we can't know all possible datatypes, we can not perform any validation here
}
}
/**
* Validation only against the datatype itself. Indicates that the value(s) should simply match the field type and
* datatype constraints.
*
* @see ValidateElement
*/
public static class BasicValidateElement extends ValidateElement {
public static final String METHOD = "basic";
/**
* @param dataType
* @see #getDatatype()
*/
public BasicValidateElement(String dataType) {
super(dataType);
}
@Override
protected void appendXML(XmlStringBuilder buf) {
buf.emptyElement(METHOD);
}
public void checkConsistency(FormField formField) {
checkListRangeConsistency(formField);
if (formField.getType() != null) {
switch (formField.getType()) {
case hidden:
case jid_multi:
case jid_single:
throw new ValidationConsistencyException(String.format(
"Field type '%1$s' is not consistent with validation method '%2$s'.",
formField.getType(), BasicValidateElement.METHOD));
default:
break;
}
}
}
}
/**
* For "list-single" or "list-multi", indicates that the user may enter a custom value (matching the datatype
* constraints) or choose from the predefined values.
*
* @see ValidateElement
*/
public static class OpenValidateElement extends ValidateElement {
public static final String METHOD = "open";
/**
* @param dataType
* @see #getDatatype()
*/
public OpenValidateElement(String dataType) {
super(dataType);
}
@Override
protected void appendXML(XmlStringBuilder buf) {
buf.emptyElement(METHOD);
}
public void checkConsistency(FormField formField) {
checkListRangeConsistency(formField);
if (formField.getType() != null) {
switch (formField.getType()) {
case hidden:
throw new ValidationConsistencyException(String.format(
"Field type '%1$s' is not consistent with validation method '%2$s'.",
formField.getType(), OpenValidateElement.METHOD));
default:
break;
}
}
}
}
/**
* Indicate that the value should fall within a certain range.
*
* @see ValidateElement
*/
public static class RangeValidateElement extends ValidateElement {
public static final String METHOD = "range";
private final String min;
private final String max;
/**
* @param dataType
* @param min the minimum allowable value. This attribute is OPTIONAL. The value depends on the datatype in use.
* @param max the maximum allowable value. This attribute is OPTIONAL. The value depends on the datatype in use.
* @see #getDatatype()
*
*/
public RangeValidateElement(String dataType, String min, String max) {
super(dataType);
this.min = min;
this.max = max;
}
@Override
protected void appendXML(XmlStringBuilder buf) {
buf.halfOpenElement(METHOD);
buf.optAttribute("min", getMin());
buf.optAttribute("max", getMax());
buf.closeEmptyElement();
}
/**
* The 'min' attribute specifies the minimum allowable value.
*
* @return the minimum allowable value. This attribute is OPTIONAL. The value depends on the datatype in use.
*/
public String getMin() {
return min;
}
/**
* The 'max' attribute specifies the maximum allowable value.
*
* @return the maximum allowable value. This attribute is OPTIONAL. The value depends on the datatype in use.
*/
public String getMax() {
return max;
}
public void checkConsistency(FormField formField) {
checkNonMultiConsistency(formField, METHOD);
if (getDatatype().equals(ValidateElement.DATATYPE_XS_STRING)) {
throw new ValidationConsistencyException(String.format(
"Field data type '%1$s' is not consistent with validation method '%2$s'.",
getDatatype(), RangeValidateElement.METHOD));
}
}
}
/**
* Indicates that the value should be restricted to a regular expression. The regular expression must be that
* defined for <a href="http://www.xmpp.org/extensions/xep-0122.html#nt-idp1501344"> POSIX extended regular
* expressions </a> including support for <a
* href="http://www.xmpp.org/extensions/xep-0122.html#nt-idp1502496">Unicode</a>.
*
* @see ValidateElement
*/
public static class RegexValidateElement extends ValidateElement {
public static final String METHOD = "regex";
private final String regex;
/**
* @param dataType
* @param regex
* @see #getDatatype()
*/
public RegexValidateElement(String dataType, String regex) {
super(dataType);
this.regex = regex;
}
/**
* the expression is that defined for POSIX extended regular expressions, including support for Unicode.
*
* @return the regex
*/
public String getRegex() {
return regex;
}
@Override
protected void appendXML(XmlStringBuilder buf) {
buf.element("regex", getRegex());
}
public void checkConsistency(FormField formField) {
checkNonMultiConsistency(formField, METHOD);
}
}
/**
* This element indicates for "list-multi", that a minimum and maximum number of options should be selected and/or
* entered.
*/
public static class ListRange implements NamedElement {
public static final String ELEMENT = "list-range";
private final Long min;
private final Long max;
/**
* The 'max' attribute specifies the maximum allowable number of selected/entered values. The 'min' attribute
* specifies the minimum allowable number of selected/entered values. Both attributes are optional and must be a
* positive integer.
*
* @param min
* @param max
*/
public ListRange(Long min, Long max) {
if (min != null && min < 0) {
throw new IllegalArgumentException("min must not be negative");
}
if (max != null && max < 0) {
throw new IllegalArgumentException("max must not be negative");
}
if (max == null && min == null) {
throw new IllegalArgumentException("Either min or max must be given");
}
this.min = min;
this.max = max;
}
public XmlStringBuilder toXML() {
XmlStringBuilder buf = new XmlStringBuilder(this);
buf.optLongAttribute("min", getMin());
buf.optLongAttribute("max", getMax());
buf.closeEmptyElement();
return buf;
}
@Override
public String getElementName() {
return ELEMENT;
}
/**
* The minimum allowable number of selected/entered values.
*
* @return a positive integer, can be null
*/
public Long getMin() {
return min;
}
/**
* The maximum allowable number of selected/entered values.
*
* @return a positive integer, can be null
*/
public Long getMax() {
return max;
}
}
/**
* The <list-range/> element SHOULD be included only when the <field/> is of type "list-multi" and SHOULD be ignored
* otherwise.
*
* @param formField
*/
protected void checkListRangeConsistency(FormField formField) {
ListRange listRange = getListRange();
if (listRange == null) {
return;
}
Long max = listRange.getMax();
Long min = listRange.getMin();
if ((max != null || min != null) && formField.getType() != FormField.Type.list_multi) {
throw new ValidationConsistencyException(
"Field type is not of type 'list-multi' while a 'list-range' is defined.");
}
}
/**
* @param formField
* @param method
*/
protected void checkNonMultiConsistency(FormField formField, String method) {
checkListRangeConsistency(formField);
if (formField.getType() != null) {
switch (formField.getType()) {
case hidden:
case jid_multi:
case list_multi:
case text_multi:
throw new ValidationConsistencyException(String.format(
"Field type '%1$s' is not consistent with validation method '%2$s'.",
formField.getType(), method));
default:
break;
}
}
}
}

View file

@ -0,0 +1,95 @@
/**
*
* Copyright 2014 Anno van Vliet
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.xdatavalidation.provider;
import java.io.IOException;
import java.util.logging.Logger;
import org.jivesoftware.smack.util.ParserUtils;
import org.jivesoftware.smackx.xdatavalidation.packet.ValidateElement;
import org.jivesoftware.smackx.xdatavalidation.packet.ValidateElement.BasicValidateElement;
import org.jivesoftware.smackx.xdatavalidation.packet.ValidateElement.EmptyValidateElement;
import org.jivesoftware.smackx.xdatavalidation.packet.ValidateElement.ListRange;
import org.jivesoftware.smackx.xdatavalidation.packet.ValidateElement.OpenValidateElement;
import org.jivesoftware.smackx.xdatavalidation.packet.ValidateElement.RangeValidateElement;
import org.jivesoftware.smackx.xdatavalidation.packet.ValidateElement.RegexValidateElement;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
/**
* Extension Provider for Data validation of forms.
*
* @author Anno van Vliet
*
*/
public class DataValidationProvider {
private static final Logger LOGGER = Logger.getLogger(DataValidationProvider.class.getName());
public static ValidateElement parse(XmlPullParser parser) throws XmlPullParserException, IOException {
final int initialDepth = parser.getDepth();
final String dataType = parser.getAttributeValue("", "datatype");
ValidateElement dataValidation = null;
ListRange listRange = null;
outerloop: while (true) {
int eventType = parser.next();
switch (eventType) {
case XmlPullParser.START_TAG:
switch (parser.getName()) {
case OpenValidateElement.METHOD:
dataValidation = new OpenValidateElement(dataType);
break;
case BasicValidateElement.METHOD:
dataValidation = new BasicValidateElement(dataType);
break;
case RangeValidateElement.METHOD:
dataValidation = new RangeValidateElement(dataType,
parser.getAttributeValue("", "min"),
parser.getAttributeValue("", "max")
);
break;
case RegexValidateElement.METHOD:
dataValidation = new RegexValidateElement(dataType,parser.nextText());
break;
case ListRange.ELEMENT:
Long min = ParserUtils.getLongAttribute(parser, "min");
Long max = ParserUtils.getLongAttribute(parser, "max");
if (min != null || max != null) {
listRange = new ListRange(min, max);
} else {
LOGGER.fine("Ignoring list-range element without min or max attribute");
}
break;
default:
break;
}
break;
case XmlPullParser.END_TAG:
if (parser.getDepth() == initialDepth) {
if (dataValidation == null) {
dataValidation = new EmptyValidateElement(dataType);
}
dataValidation.setListRange(listRange);
break outerloop;
}
break;
}
}
return dataValidation;
}
}