1
0
Fork 0
mirror of https://github.com/vanitasvitae/Smack.git synced 2024-11-29 07:22:07 +01:00

Re-work data form API

Apply builder pattern to form fields and replace getVariable() with
getFieldName(). Refer to the field name as "field name" instead of
"variable" everyone, just as XEP-0004 does.

Improve the high-level form API: introduce FilledForm and FillableForm
which perform stronger validation and consistency checks.

Also add FormFieldRegistry to enable processing of 'submit' forms
where the form field types are omitted.

Smack also now does omit the form field type declaration on 'submit'
type forms, as it is allowed by XEP-0004.
This commit is contained in:
Florian Schmaus 2020-05-13 20:14:41 +02:00
parent 3270c113c5
commit 77e26fc575
97 changed files with 3809 additions and 2427 deletions

View file

@ -51,6 +51,7 @@ Smack Extensions and currently supported XEPs of smack-extensions
| Result Set Management | [XEP-0059](https://xmpp.org/extensions/xep-0059.html) | n/a | Page through and otherwise manage the receipt of large result sets | | Result Set Management | [XEP-0059](https://xmpp.org/extensions/xep-0059.html) | n/a | Page through and otherwise manage the receipt of large result sets |
| [PubSub](pubsub.md) | [XEP-0060](https://xmpp.org/extensions/xep-0060.html) | n/a | Generic publish and subscribe functionality. | | [PubSub](pubsub.md) | [XEP-0060](https://xmpp.org/extensions/xep-0060.html) | n/a | Generic publish and subscribe functionality. |
| SOCKS5 Bytestreams | [XEP-0065](https://xmpp.org/extensions/xep-0065.html) | n/a | Out-of-band bytestream between any two XMPP entities. | | SOCKS5 Bytestreams | [XEP-0065](https://xmpp.org/extensions/xep-0065.html) | n/a | Out-of-band bytestream between any two XMPP entities. |
| Field Standardization for Data Forms | [XEP-0068](https://xmpp.org/extensions/xep-0068.html) | n/a | Standardized field variables used in the context of jabber:x:data forms. |
| [XHTML-IM](xhtml.md) | [XEP-0071](https://xmpp.org/extensions/xep-0071.html) | n/a | Allows send and receiving formatted messages using XHTML. | | [XHTML-IM](xhtml.md) | [XEP-0071](https://xmpp.org/extensions/xep-0071.html) | n/a | Allows send and receiving formatted messages using XHTML. |
| In-Band Registration | [XEP-0077](https://xmpp.org/extensions/xep-0077.html) | n/a | In-band registration with XMPP services. | | In-Band Registration | [XEP-0077](https://xmpp.org/extensions/xep-0077.html) | n/a | In-band registration with XMPP services. |
| Advanced Message Processing | [XEP-0079](https://xmpp.org/extensions/xep-0079.html) | n/a | Enables entities to request, and servers to perform, advanced processing of XMPP message stanzas. | | Advanced Message Processing | [XEP-0079](https://xmpp.org/extensions/xep-0079.html) | n/a | Enables entities to request, and servers to perform, advanced processing of XMPP message stanzas. |

View file

@ -1,6 +1,6 @@
/** /**
* *
* Copyright 2019 Florian Schmaus * Copyright 2019-2020 Florian Schmaus
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -30,6 +30,10 @@ public class SmackParsingException extends Exception {
super(exception); super(exception);
} }
public SmackParsingException(String message) {
super(message);
}
public static class SmackTextParseException extends SmackParsingException { public static class SmackTextParseException extends SmackParsingException {
/** /**
* *

View file

@ -18,9 +18,12 @@ package org.jivesoftware.smack.util;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
public class CollectionUtil { public class CollectionUtil {
@ -59,6 +62,20 @@ public class CollectionUtil {
return new ArrayList<>(collection); return new ArrayList<>(collection);
} }
public static <T> List<T> cloneAndSeal(Collection<? extends T> collection) {
if (collection == null) {
return Collections.emptyList();
}
ArrayList<T> clone = newListWith(collection);
return Collections.unmodifiableList(clone);
}
public static <K, V> Map<K, V> cloneAndSeal(Map<K, V> map) {
Map<K, V> clone = new HashMap<>(map);
return Collections.unmodifiableMap(clone);
}
public static <T> Set<T> newSetWith(Collection<? extends T> collection) { public static <T> Set<T> newSetWith(Collection<? extends T> collection) {
if (collection == null) { if (collection == null) {
return null; return null;

View file

@ -20,8 +20,10 @@ package org.jivesoftware.smack.util;
import java.io.IOException; import java.io.IOException;
import java.nio.CharBuffer; import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Iterator; import java.util.Iterator;
import java.util.List;
import java.util.Random; import java.util.Random;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -591,4 +593,11 @@ public class StringUtils {
} }
return appendable.append('\n'); return appendable.append('\n');
} }
public static final String PORTABLE_NEWLINE_REGEX = "\\r?\\n";
public static List<String> splitLinesPortable(String input) {
String[] lines = input.split(PORTABLE_NEWLINE_REGEX);
return Arrays.asList(lines);
}
} }

View file

@ -77,6 +77,10 @@ public class XmlStringBuilder implements Appendable, CharSequence, Element {
.build(); .build();
} }
public XmlEnvironment getXmlEnvironment() {
return effectiveXmlEnvironment;
}
public XmlStringBuilder escapedElement(String name, String escapedContent) { public XmlStringBuilder escapedElement(String name, String escapedContent) {
assert escapedContent != null; assert escapedContent != null;
openElement(name); openElement(name);
@ -493,6 +497,13 @@ public class XmlStringBuilder implements Appendable, CharSequence, Element {
return this; return this;
} }
public XmlStringBuilder optAppend(Collection<? extends Element> elements) {
if (elements != null) {
append(elements);
}
return this;
}
public XmlStringBuilder optTextChild(CharSequence sqc, NamedElement parentElement) { public XmlStringBuilder optTextChild(CharSequence sqc, NamedElement parentElement) {
if (sqc == null) { if (sqc == null) {
return closeEmptyElement(); return closeEmptyElement();

View file

@ -278,8 +278,9 @@ public final class MamManager extends Manager {
if (dataForm != null) { if (dataForm != null) {
return dataForm; return dataForm;
} }
dataForm = getNewMamForm(); DataForm.Builder dataFormBuilder = getNewMamForm();
dataForm.addFields(formFields.values()); dataFormBuilder.addFields(formFields.values());
dataForm = dataFormBuilder.build();
return dataForm; return dataForm;
} }
@ -330,7 +331,7 @@ public final class MamManager extends Manager {
} }
FormField formField = getWithFormField(withJid); FormField formField = getWithFormField(withJid);
formFields.put(formField.getVariable(), formField); formFields.put(formField.getFieldName(), formField);
return this; return this;
} }
@ -341,9 +342,9 @@ public final class MamManager extends Manager {
} }
FormField formField = FormField.builder(FORM_FIELD_START) FormField formField = FormField.builder(FORM_FIELD_START)
.addValue(start) .setValue(start)
.build(); .build();
formFields.put(formField.getVariable(), formField); formFields.put(formField.getFieldName(), formField);
FormField endFormField = formFields.get(FORM_FIELD_END); FormField endFormField = formFields.get(FORM_FIELD_END);
if (endFormField != null) { if (endFormField != null) {
@ -369,9 +370,9 @@ public final class MamManager extends Manager {
} }
FormField formField = FormField.builder(FORM_FIELD_END) FormField formField = FormField.builder(FORM_FIELD_END)
.addValue(end) .setValue(end)
.build(); .build();
formFields.put(formField.getVariable(), formField); formFields.put(formField.getFieldName(), formField);
FormField startFormField = formFields.get(FORM_FIELD_START); FormField startFormField = formFields.get(FORM_FIELD_START);
if (startFormField != null) { if (startFormField != null) {
@ -418,7 +419,7 @@ public final class MamManager extends Manager {
} }
public Builder withAdditionalFormField(FormField formField) { public Builder withAdditionalFormField(FormField formField) {
formFields.put(formField.getVariable(), formField); formFields.put(formField.getFieldName(), formField);
return this; return this;
} }
@ -483,7 +484,7 @@ public final class MamManager extends Manager {
private static FormField getWithFormField(Jid withJid) { private static FormField getWithFormField(Jid withJid) {
return FormField.builder(FORM_FIELD_WITH) return FormField.builder(FORM_FIELD_WITH)
.addValue(withJid.toString()) .setValue(withJid.toString())
.build(); .build();
} }
@ -718,9 +719,9 @@ public final class MamManager extends Manager {
throw new SmackException.FeatureNotSupportedException(ADVANCED_CONFIG_NODE, archiveAddress); throw new SmackException.FeatureNotSupportedException(ADVANCED_CONFIG_NODE, archiveAddress);
} }
private static DataForm getNewMamForm() { private static DataForm.Builder getNewMamForm() {
FormField field = FormField.hiddenFormType(MamElements.NAMESPACE); FormField field = FormField.buildHiddenFormType(MamElements.NAMESPACE);
DataForm form = new DataForm(DataForm.Type.submit); DataForm.Builder form = DataForm.builder();
form.addField(field); form.addField(field);
return form; return form;
} }

View file

@ -24,6 +24,7 @@ import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smackx.pubsub.packet.PubSub; import org.jivesoftware.smackx.pubsub.packet.PubSub;
import org.jivesoftware.smackx.xdata.FormField; import org.jivesoftware.smackx.xdata.FormField;
import org.jivesoftware.smackx.xdata.TextSingleFormField;
import org.jivesoftware.smackx.xdata.packet.DataForm; import org.jivesoftware.smackx.xdata.packet.DataForm;
import org.jxmpp.jid.Jid; import org.jxmpp.jid.Jid;
@ -98,24 +99,23 @@ public class EnablePushNotificationsIQ extends IQ {
xml.rightAngleBracket(); xml.rightAngleBracket();
if (publishOptions != null) { if (publishOptions != null) {
DataForm dataForm = new DataForm(DataForm.Type.submit); DataForm.Builder dataForm = DataForm.builder();
// TODO: Shouldn't this use some potentially existing PubSub API? Also FORM_TYPE fields are usually of type // TODO: Shouldn't this use some potentially existing PubSub API? Also FORM_TYPE fields are usually of type
// 'hidden', but the examples in XEP-0357 do also not set the value to hidden and FORM_TYPE itself appears // 'hidden', but the examples in XEP-0357 do also not set the value to hidden and FORM_TYPE itself appears
// to be more convention than specification. // to be more convention than specification.
FormField.Builder formTypeField = FormField.builder("FORM_TYPE"); FormField formTypeField = FormField.buildHiddenFormType(PubSub.NAMESPACE + "#publish-options");
formTypeField.addValue(PubSub.NAMESPACE + "#publish-options"); dataForm.addField(formTypeField);
dataForm.addField(formTypeField.build());
Iterator<Map.Entry<String, String>> publishOptionsIterator = publishOptions.entrySet().iterator(); Iterator<Map.Entry<String, String>> publishOptionsIterator = publishOptions.entrySet().iterator();
while (publishOptionsIterator.hasNext()) { while (publishOptionsIterator.hasNext()) {
Map.Entry<String, String> pairVariableValue = publishOptionsIterator.next(); Map.Entry<String, String> pairVariableValue = publishOptionsIterator.next();
FormField.Builder field = FormField.builder(pairVariableValue.getKey()); TextSingleFormField.Builder field = FormField.builder(pairVariableValue.getKey());
field.addValue(pairVariableValue.getValue()); field.setValue(pairVariableValue.getValue());
dataForm.addField(field.build()); dataForm.addField(field.build());
} }
xml.append(dataForm); xml.append(dataForm.build());
} }
return xml; return xml;

View file

@ -1,6 +1,6 @@
/** /**
* *
* Copyright 2016 Fernando Ramirez, 2018 Florian Schmaus * Copyright 2016 Fernando Ramirez, 2018-2020 Florian Schmaus
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -34,7 +34,7 @@ import org.jxmpp.util.XmppDateTime;
public class FiltersTest extends MamTest { public class FiltersTest extends MamTest {
private static String getMamXMemberWith(List<String> fieldsNames, List<? extends CharSequence> fieldsValues) { private static String getMamXMemberWith(List<String> fieldsNames, List<? extends CharSequence> fieldsValues) {
String xml = "<x xmlns='jabber:x:data' type='submit'>" + "<field var='FORM_TYPE' type='hidden'>" + "<value>" String xml = "<x xmlns='jabber:x:data' type='submit'>" + "<field var='FORM_TYPE'>" + "<value>"
+ MamElements.NAMESPACE + "</value>" + "</field>"; + MamElements.NAMESPACE + "</value>" + "</field>";
for (int i = 0; i < fieldsNames.size() && i < fieldsValues.size(); i++) { for (int i = 0; i < fieldsNames.size() && i < fieldsValues.size(); i++) {

View file

@ -85,7 +85,7 @@ public class MamQueryIQProviderTest {
assertEquals(fields2.get(1).getType(), FormField.Type.jid_single); assertEquals(fields2.get(1).getType(), FormField.Type.jid_single);
assertEquals(fields2.get(2).getType(), FormField.Type.text_single); assertEquals(fields2.get(2).getType(), FormField.Type.text_single);
assertEquals(fields2.get(2).getValues(), new ArrayList<>()); assertEquals(fields2.get(2).getValues(), new ArrayList<>());
assertEquals(fields2.get(4).getVariable(), "urn:example:xmpp:free-text-search"); assertEquals(fields2.get(4).getFieldName(), "urn:example:xmpp:free-text-search");
} }
} }

View file

@ -49,7 +49,8 @@ public class MamTest extends SmackTestSuite {
IllegalArgumentException, InvocationTargetException { IllegalArgumentException, InvocationTargetException {
Method methodGetNewMamForm = MamManager.class.getDeclaredMethod("getNewMamForm"); Method methodGetNewMamForm = MamManager.class.getDeclaredMethod("getNewMamForm");
methodGetNewMamForm.setAccessible(true); methodGetNewMamForm.setAccessible(true);
return (DataForm) methodGetNewMamForm.invoke(mamManager); DataForm.Builder dataFormBuilder = (DataForm.Builder) methodGetNewMamForm.invoke(mamManager);
return dataFormBuilder.build();
} }
} }

View file

@ -30,7 +30,7 @@ import org.junit.jupiter.api.Test;
public class PagingTest extends MamTest { public class PagingTest extends MamTest {
private static final String pagingStanza = "<iq id='sarasa' type='set'>" + "<query xmlns='urn:xmpp:mam:1' queryid='testid'>" private static final String pagingStanza = "<iq id='sarasa' type='set'>" + "<query xmlns='urn:xmpp:mam:1' queryid='testid'>"
+ "<x xmlns='jabber:x:data' type='submit'>" + "<field var='FORM_TYPE' type='hidden'>" + "<x xmlns='jabber:x:data' type='submit'>" + "<field var='FORM_TYPE'>"
+ "<value>urn:xmpp:mam:1</value>" + "</field>" + "</x>" + "<set xmlns='http://jabber.org/protocol/rsm'>" + "<value>urn:xmpp:mam:1</value>" + "</field>" + "</x>" + "<set xmlns='http://jabber.org/protocol/rsm'>"
+ "<max>10</max>" + "</set>" + "</query>" + "</iq>"; + "<max>10</max>" + "</set>" + "</query>" + "</iq>";

View file

@ -41,7 +41,7 @@ import org.jxmpp.jid.impl.JidCreate;
public class QueryArchiveTest extends MamTest { public class QueryArchiveTest extends MamTest {
private static final String mamSimpleQueryIQ = "<iq id='sarasa' type='set'>" + "<query xmlns='urn:xmpp:mam:1' queryid='testid'>" private static final String mamSimpleQueryIQ = "<iq id='sarasa' type='set'>" + "<query xmlns='urn:xmpp:mam:1' queryid='testid'>"
+ "<x xmlns='jabber:x:data' type='submit'>" + "<field var='FORM_TYPE' type='hidden'>" + "<value>" + "<x xmlns='jabber:x:data' type='submit'>" + "<field var='FORM_TYPE'>" + "<value>"
+ MamElements.NAMESPACE + "</value>" + "</field>" + "</x>" + "</query>" + "</iq>"; + MamElements.NAMESPACE + "</value>" + "</field>" + "</x>" + "</query>" + "</iq>";
private static final String mamQueryResultExample = "<message to='hag66@shakespeare.lit/pda' from='coven@chat.shakespeare.lit' id='iasd207'>" private static final String mamQueryResultExample = "<message to='hag66@shakespeare.lit/pda' from='coven@chat.shakespeare.lit' id='iasd207'>"

View file

@ -1,6 +1,6 @@
/** /**
* *
* Copyright 2016 Fernando Ramirez, 2018-2019 Florian Schmaus * Copyright 2016 Fernando Ramirez, 2018-2020 Florian Schmaus
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -31,7 +31,7 @@ import org.junit.jupiter.api.Test;
public class ResultsLimitTest extends MamTest { public class ResultsLimitTest extends MamTest {
private static final String resultsLimitStanza = "<iq id='sarasa' type='set'>" + "<query xmlns='urn:xmpp:mam:1' queryid='testid'>" private static final String resultsLimitStanza = "<iq id='sarasa' type='set'>" + "<query xmlns='urn:xmpp:mam:1' queryid='testid'>"
+ "<x xmlns='jabber:x:data' type='submit'>" + "<field var='FORM_TYPE' type='hidden'>" + "<value>" + "<x xmlns='jabber:x:data' type='submit'>" + "<field var='FORM_TYPE'>" + "<value>"
+ MamElements.NAMESPACE + "</value>" + "</field>" + "</x>" + "<set xmlns='http://jabber.org/protocol/rsm'>" + MamElements.NAMESPACE + "</value>" + "</field>" + "</x>" + "<set xmlns='http://jabber.org/protocol/rsm'>"
+ "<max>10</max>" + "</set>" + "</query>" + "</iq>"; + "<max>10</max>" + "</set>" + "</query>" + "</iq>";

View file

@ -1,6 +1,6 @@
/** /**
* *
* Copyright 2016 Fernando Ramirez, 2018-2019 Florian Schmaus * Copyright 2016 Fernando Ramirez, 2018-2020 Florian Schmaus
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -28,16 +28,17 @@ import org.jivesoftware.smackx.xdata.FormField;
import org.jivesoftware.smackx.xdata.packet.DataForm; import org.jivesoftware.smackx.xdata.packet.DataForm;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.jxmpp.jid.JidTestUtil;
public class RetrieveFormFieldsTest extends MamTest { public class RetrieveFormFieldsTest extends MamTest {
private static final String retrieveFormFieldStanza = "<iq id='sarasa' type='get'>" + "<query xmlns='" + MamElements.NAMESPACE private static final String retrieveFormFieldStanza = "<iq id='sarasa' type='get'>" + "<query xmlns='" + MamElements.NAMESPACE
+ "' queryid='testid'></query>" + "</iq>"; + "' queryid='testid'></query>" + "</iq>";
private static final String additionalFieldsStanza = "<x xmlns='jabber:x:data' type='submit'>" + "<field var='FORM_TYPE' type='hidden'>" private static final String additionalFieldsStanza = "<x xmlns='jabber:x:data' type='submit'>" + "<field var='FORM_TYPE'>"
+ "<value>" + MamElements.NAMESPACE + "</value>" + "</field>" + "<value>" + MamElements.NAMESPACE + "</value>" + "</field>"
+ "<field var='urn:example:xmpp:free-text-search'>" + "<value>Hi</value>" + "</field>" + "<field var='urn:example:xmpp:free-text-search'>" + "<value>Hi</value>" + "</field>"
+ "<field var='urn:example:xmpp:stanza-content' type='jid-single'>" + "<value>Hi2</value>" + "</field>" + "<field var='urn:example:xmpp:stanza-content'>" + "<value>one@exampleone.org</value>" + "</field>"
+ "</x>"; + "</x>";
@Test @Test
@ -51,13 +52,11 @@ public class RetrieveFormFieldsTest extends MamTest {
@Test @Test
public void checkAddAdditionalFieldsStanza() throws Exception { public void checkAddAdditionalFieldsStanza() throws Exception {
FormField field1 = FormField.builder("urn:example:xmpp:free-text-search") FormField field1 = FormField.builder("urn:example:xmpp:free-text-search")
.setType(FormField.Type.text_single) .setValue("Hi")
.addValue("Hi")
.build(); .build();
FormField field2 = FormField.builder("urn:example:xmpp:stanza-content") FormField field2 = FormField.jidSingleBuilder("urn:example:xmpp:stanza-content")
.setType(FormField.Type.jid_single) .setValue(JidTestUtil.BARE_JID_1)
.addValue("Hi2")
.build(); .build();
MamQueryArgs mamQueryArgs = MamQueryArgs.builder() MamQueryArgs mamQueryArgs = MamQueryArgs.builder()

View file

@ -1,6 +1,6 @@
/** /**
* *
* Copyright 2016-2019 Florian Schmaus * Copyright 2016-2020 Florian Schmaus
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -29,7 +29,7 @@ import org.jivesoftware.smack.XMPPException.XMPPErrorException;
import org.jivesoftware.smackx.commands.AdHocCommandManager; import org.jivesoftware.smackx.commands.AdHocCommandManager;
import org.jivesoftware.smackx.commands.RemoteCommand; import org.jivesoftware.smackx.commands.RemoteCommand;
import org.jivesoftware.smackx.xdata.Form; import org.jivesoftware.smackx.xdata.form.FillableForm;
import org.jxmpp.jid.EntityBareJid; import org.jxmpp.jid.EntityBareJid;
import org.jxmpp.jid.Jid; import org.jxmpp.jid.Jid;
@ -72,7 +72,7 @@ public class ServiceAdministrationManager extends Manager {
RemoteCommand command = addUser(); RemoteCommand command = addUser();
command.execute(); command.execute();
Form answerForm = command.getForm().createAnswerForm(); FillableForm answerForm = new FillableForm(command.getForm());
answerForm.setAnswer("accountjid", userJid); answerForm.setAnswer("accountjid", userJid);
answerForm.setAnswer("password", password); answerForm.setAnswer("password", password);
@ -101,7 +101,7 @@ public class ServiceAdministrationManager extends Manager {
RemoteCommand command = deleteUser(); RemoteCommand command = deleteUser();
command.execute(); command.execute();
Form answerForm = command.getForm().createAnswerForm(); FillableForm answerForm = new FillableForm(command.getForm());
answerForm.setAnswer("accountjids", jidsToDelete); answerForm.setAnswer("accountjids", jidsToDelete);

View file

@ -1,6 +1,6 @@
/** /**
* *
* Copyright © 2009 Jonas Ådahl, 2011-2019 Florian Schmaus * Copyright © 2009 Jonas Ådahl, 2011-2020 Florian Schmaus
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -22,11 +22,13 @@ import java.security.NoSuchAlgorithmException;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Queue; import java.util.Queue;
import java.util.Set;
import java.util.SortedSet; import java.util.SortedSet;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.WeakHashMap; import java.util.WeakHashMap;
@ -49,7 +51,6 @@ import org.jivesoftware.smack.filter.PresenceTypeFilter;
import org.jivesoftware.smack.filter.StanzaExtensionFilter; import org.jivesoftware.smack.filter.StanzaExtensionFilter;
import org.jivesoftware.smack.filter.StanzaFilter; import org.jivesoftware.smack.filter.StanzaFilter;
import org.jivesoftware.smack.filter.StanzaTypeFilter; import org.jivesoftware.smack.filter.StanzaTypeFilter;
import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.PresenceBuilder; import org.jivesoftware.smack.packet.PresenceBuilder;
@ -601,7 +602,7 @@ public final class EntityCapsManager extends Manager {
return false; return false;
// step 3.5 check for well-formed packet extensions // step 3.5 check for well-formed packet extensions
if (verifyPacketExtensions(info)) if (!verifyPacketExtensions(info))
return false; return false;
String calculatedVer = generateVerificationString(info, hash).version; String calculatedVer = generateVerificationString(info, hash).version;
@ -613,28 +614,30 @@ public final class EntityCapsManager extends Manager {
} }
/** /**
* Verify that the given discovery info is not ill-formed.
* *
* @param info TODO javadoc me please * @param info the discovery info to verify.
* @return true if the stanza extensions is ill-formed * @return true if the stanza extensions is not ill-formed
*/ */
protected static boolean verifyPacketExtensions(DiscoverInfo info) { private static boolean verifyPacketExtensions(DiscoverInfo info) {
List<FormField> foundFormTypes = new LinkedList<>(); Set<String> foundFormTypes = new HashSet<>();
for (ExtensionElement pe : info.getExtensions()) { List<DataForm> dataForms = info.getExtensions(DataForm.class);
if (pe.getNamespace().equals(DataForm.NAMESPACE)) { for (DataForm dataForm : dataForms) {
DataForm df = (DataForm) pe; FormField formFieldTypeField = dataForm.getHiddenFormTypeField();
for (FormField f : df.getFields()) { if (formFieldTypeField == null) {
if (f.getVariable().equals("FORM_TYPE")) { continue;
for (FormField fft : foundFormTypes) {
if (f.equals(fft))
return true;
}
foundFormTypes.add(f);
}
}
}
} }
String type = formFieldTypeField.getFirstValue();
boolean noDuplicate = foundFormTypes.add(type);
if (!noDuplicate) {
// Ill-formed extension: duplicate forms (by form field type string).
return false; return false;
} }
}
return true;
}
protected static CapsVersionAndHash generateVerificationString(DiscoverInfoView discoverInfo) { protected static CapsVersionAndHash generateVerificationString(DiscoverInfoView discoverInfo) {
return generateVerificationString(discoverInfo, null); return generateVerificationString(discoverInfo, null);
@ -718,12 +721,12 @@ public final class EntityCapsManager extends Manager {
SortedSet<FormField> fs = new TreeSet<>(new Comparator<FormField>() { SortedSet<FormField> fs = new TreeSet<>(new Comparator<FormField>() {
@Override @Override
public int compare(FormField f1, FormField f2) { public int compare(FormField f1, FormField f2) {
return f1.getVariable().compareTo(f2.getVariable()); return f1.getFieldName().compareTo(f2.getFieldName());
} }
}); });
for (FormField f : extendedInfo.getFields()) { for (FormField f : extendedInfo.getFields()) {
if (!f.getVariable().equals("FORM_TYPE")) { if (!f.getFieldName().equals("FORM_TYPE")) {
fs.add(f); fs.add(f);
} }
} }
@ -739,7 +742,7 @@ public final class EntityCapsManager extends Manager {
// 3. For each <value/> element, append the XML character data, // 3. For each <value/> element, append the XML character data,
// followed by the '<' character. // followed by the '<' character.
for (FormField f : fs) { for (FormField f : fs) {
sb.append(f.getVariable()); sb.append(f.getFieldName());
sb.append('<'); sb.append('<');
formFieldValuesToCaps(f.getValues(), sb); formFieldValuesToCaps(f.getValues(), sb);
} }
@ -763,7 +766,7 @@ public final class EntityCapsManager extends Manager {
return new CapsVersionAndHash(version, hash); return new CapsVersionAndHash(version, hash);
} }
private static void formFieldValuesToCaps(List<CharSequence> i, StringBuilder sb) { private static void formFieldValuesToCaps(List<? extends CharSequence> i, StringBuilder sb) {
SortedSet<CharSequence> fvs = new TreeSet<>(); SortedSet<CharSequence> fvs = new TreeSet<>();
fvs.addAll(i); fvs.addAll(i);
for (CharSequence fv : fvs) { for (CharSequence fv : fvs) {

View file

@ -24,7 +24,8 @@ import org.jivesoftware.smack.XMPPException.XMPPErrorException;
import org.jivesoftware.smack.packet.StanzaError; import org.jivesoftware.smack.packet.StanzaError;
import org.jivesoftware.smackx.commands.packet.AdHocCommandData; import org.jivesoftware.smackx.commands.packet.AdHocCommandData;
import org.jivesoftware.smackx.xdata.Form; import org.jivesoftware.smackx.xdata.form.FillableForm;
import org.jivesoftware.smackx.xdata.packet.DataForm;
import org.jxmpp.jid.Jid; import org.jxmpp.jid.Jid;
@ -188,13 +189,8 @@ public abstract class AdHocCommand {
* @return the form of the current stage to fill out or the result of the * @return the form of the current stage to fill out or the result of the
* execution. * execution.
*/ */
public Form getForm() { public DataForm getForm() {
if (data.getForm() == null) { return data.getForm();
return null;
}
else {
return new Form(data.getForm());
}
} }
/** /**
@ -205,8 +201,8 @@ public abstract class AdHocCommand {
* @param form the form of the current stage to fill out or the result of the * @param form the form of the current stage to fill out or the result of the
* execution. * execution.
*/ */
protected void setForm(Form form) { protected void setForm(DataForm form) {
data.setForm(form.getDataFormToSend()); data.setForm(form);
} }
/** /**
@ -234,7 +230,7 @@ public abstract class AdHocCommand {
* @throws NotConnectedException if the XMPP connection is not connected. * @throws NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted. * @throws InterruptedException if the calling thread was interrupted.
*/ */
public abstract void next(Form response) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException; public abstract void next(FillableForm response) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException;
/** /**
* Completes the command execution with the information provided in the * Completes the command execution with the information provided in the
@ -250,7 +246,7 @@ public abstract class AdHocCommand {
* @throws NotConnectedException if the XMPP connection is not connected. * @throws NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted. * @throws InterruptedException if the calling thread was interrupted.
*/ */
public abstract void complete(Form response) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException; public abstract void complete(FillableForm response) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException;
/** /**
* Goes to the previous stage. The requester is asking to re-send the * Goes to the previous stage. The requester is asking to re-send the

View file

@ -52,7 +52,8 @@ import org.jivesoftware.smackx.disco.AbstractNodeInformationProvider;
import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
import org.jivesoftware.smackx.disco.packet.DiscoverInfo; import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
import org.jivesoftware.smackx.disco.packet.DiscoverItems; import org.jivesoftware.smackx.disco.packet.DiscoverItems;
import org.jivesoftware.smackx.xdata.Form; import org.jivesoftware.smackx.xdata.form.FillableForm;
import org.jivesoftware.smackx.xdata.packet.DataForm;
import org.jxmpp.jid.Jid; import org.jxmpp.jid.Jid;
@ -462,7 +463,8 @@ public final class AdHocCommandManager extends Manager {
if (Action.next.equals(action)) { if (Action.next.equals(action)) {
command.incrementStage(); command.incrementStage();
command.next(new Form(requestData.getForm())); DataForm dataForm = requestData.getForm();
command.next(new FillableForm(dataForm));
if (command.isLastStage()) { if (command.isLastStage()) {
// If it is the last stage then the command is // If it is the last stage then the command is
// completed // completed
@ -475,7 +477,8 @@ public final class AdHocCommandManager extends Manager {
} }
else if (Action.complete.equals(action)) { else if (Action.complete.equals(action)) {
command.incrementStage(); command.incrementStage();
command.complete(new Form(requestData.getForm())); DataForm dataForm = requestData.getForm();
command.complete(new FillableForm(dataForm));
response.setStatus(Status.completed); response.setStatus(Status.completed);
// Remove the completed session // Remove the completed session
executingCommands.remove(sessionId); executingCommands.remove(sessionId);

View file

@ -23,7 +23,8 @@ import org.jivesoftware.smack.XMPPException.XMPPErrorException;
import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smackx.commands.packet.AdHocCommandData; import org.jivesoftware.smackx.commands.packet.AdHocCommandData;
import org.jivesoftware.smackx.xdata.Form; import org.jivesoftware.smackx.xdata.form.FillableForm;
import org.jivesoftware.smackx.xdata.packet.DataForm;
import org.jxmpp.jid.Jid; import org.jxmpp.jid.Jid;
@ -80,8 +81,8 @@ public class RemoteCommand extends AdHocCommand {
} }
@Override @Override
public void complete(Form form) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { public void complete(FillableForm form) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
executeAction(Action.complete, form); executeAction(Action.complete, form.getDataFormToSubmit());
} }
@Override @Override
@ -100,13 +101,13 @@ public class RemoteCommand extends AdHocCommand {
* @throws NotConnectedException if the XMPP connection is not connected. * @throws NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted. * @throws InterruptedException if the calling thread was interrupted.
*/ */
public void execute(Form form) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { public void execute(FillableForm form) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
executeAction(Action.execute, form); executeAction(Action.execute, form.getDataFormToSubmit());
} }
@Override @Override
public void next(Form form) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { public void next(FillableForm form) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
executeAction(Action.next, form); executeAction(Action.next, form.getDataFormToSubmit());
} }
@Override @Override
@ -130,7 +131,7 @@ public class RemoteCommand extends AdHocCommand {
* @throws NotConnectedException if the XMPP connection is not connected. * @throws NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted. * @throws InterruptedException if the calling thread was interrupted.
*/ */
private void executeAction(Action action, Form form) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { private void executeAction(Action action, DataForm form) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
// TODO: Check that all the required fields of the form were filled, if // TODO: Check that all the required fields of the form were filled, if
// TODO: not throw the corresponding exception. This will make a faster response, // TODO: not throw the corresponding exception. This will make a faster response,
// TODO: since the request is stopped before it's sent. // TODO: since the request is stopped before it's sent.
@ -140,10 +141,7 @@ public class RemoteCommand extends AdHocCommand {
data.setNode(getNode()); data.setNode(getNode());
data.setSessionID(sessionID); data.setSessionID(sessionID);
data.setAction(action); data.setAction(action);
data.setForm(form);
if (form != null) {
data.setForm(form.getDataFormToSend());
}
AdHocCommandData responseData = null; AdHocCommandData responseData = null;
try { try {

View file

@ -42,6 +42,7 @@ import org.jivesoftware.smackx.filetransfer.FileTransferException.NoAcceptableTr
import org.jivesoftware.smackx.filetransfer.FileTransferException.NoStreamMethodsOfferedException; import org.jivesoftware.smackx.filetransfer.FileTransferException.NoStreamMethodsOfferedException;
import org.jivesoftware.smackx.si.packet.StreamInitiation; import org.jivesoftware.smackx.si.packet.StreamInitiation;
import org.jivesoftware.smackx.xdata.FormField; import org.jivesoftware.smackx.xdata.FormField;
import org.jivesoftware.smackx.xdata.ListSingleFormField;
import org.jivesoftware.smackx.xdata.packet.DataForm; import org.jivesoftware.smackx.xdata.packet.DataForm;
import org.jxmpp.jid.Jid; import org.jxmpp.jid.Jid;
@ -189,7 +190,7 @@ public final class FileTransferNegotiator extends Manager {
public StreamNegotiator selectStreamNegotiator( public StreamNegotiator selectStreamNegotiator(
FileTransferRequest request) throws NotConnectedException, NoStreamMethodsOfferedException, NoAcceptableTransferMechanisms, InterruptedException { FileTransferRequest request) throws NotConnectedException, NoStreamMethodsOfferedException, NoAcceptableTransferMechanisms, InterruptedException {
StreamInitiation si = request.getStreamInitiation(); StreamInitiation si = request.getStreamInitiation();
FormField streamMethodField = getStreamMethodField(si ListSingleFormField streamMethodField = getStreamMethodField(si
.getFeatureNegotiationForm()); .getFeatureNegotiationForm());
if (streamMethodField == null) { if (streamMethodField == null) {
@ -216,11 +217,11 @@ public final class FileTransferNegotiator extends Manager {
return selectedStreamNegotiator; return selectedStreamNegotiator;
} }
private static FormField getStreamMethodField(DataForm form) { private static ListSingleFormField getStreamMethodField(DataForm form) {
return form.getField(STREAM_DATA_FIELD_NAME); return (ListSingleFormField) form.getField(STREAM_DATA_FIELD_NAME);
} }
private StreamNegotiator getNegotiator(final FormField field) private StreamNegotiator getNegotiator(final ListSingleFormField field)
throws NoAcceptableTransferMechanisms { throws NoAcceptableTransferMechanisms {
String variable; String variable;
boolean isByteStream = false; boolean isByteStream = false;
@ -359,16 +360,15 @@ public final class FileTransferNegotiator extends Manager {
} }
private static DataForm createDefaultInitiationForm() { private static DataForm createDefaultInitiationForm() {
DataForm form = new DataForm(DataForm.Type.form); DataForm.Builder form = DataForm.builder()
FormField.Builder fieldBuilder = FormField.builder(); .setType(DataForm.Type.form);
fieldBuilder.setFieldName(STREAM_DATA_FIELD_NAME) ListSingleFormField.Builder fieldBuilder = FormField.listSingleBuilder(STREAM_DATA_FIELD_NAME);
.setType(FormField.Type.list_single);
if (!IBB_ONLY) { if (!IBB_ONLY) {
fieldBuilder.addOption(Bytestream.NAMESPACE); fieldBuilder.addOption(Bytestream.NAMESPACE);
} }
fieldBuilder.addOption(DataPacketExtension.NAMESPACE); fieldBuilder.addOption(DataPacketExtension.NAMESPACE);
form.addField(fieldBuilder.build()); form.addField(fieldBuilder.build());
return form; return form.build();
} }
} }

View file

@ -33,6 +33,7 @@ import org.jivesoftware.smack.util.EventManger.Callback;
import org.jivesoftware.smackx.si.packet.StreamInitiation; import org.jivesoftware.smackx.si.packet.StreamInitiation;
import org.jivesoftware.smackx.xdata.FormField; import org.jivesoftware.smackx.xdata.FormField;
import org.jivesoftware.smackx.xdata.ListSingleFormField;
import org.jivesoftware.smackx.xdata.packet.DataForm; import org.jivesoftware.smackx.xdata.packet.DataForm;
import org.jxmpp.jid.Jid; import org.jxmpp.jid.Jid;
@ -80,13 +81,13 @@ public abstract class StreamNegotiator extends Manager {
response.setType(IQ.Type.result); response.setType(IQ.Type.result);
response.setStanzaId(streamInitiationOffer.getStanzaId()); response.setStanzaId(streamInitiationOffer.getStanzaId());
DataForm form = new DataForm(DataForm.Type.submit); DataForm.Builder form = DataForm.builder();
FormField.Builder field = FormField.builder( ListSingleFormField.Builder field = FormField.listSingleBuilder(
FileTransferNegotiator.STREAM_DATA_FIELD_NAME); FileTransferNegotiator.STREAM_DATA_FIELD_NAME);
field.addValue(namespace); field.setValue(namespace);
form.addField(field.build()); form.addField(field.build());
response.setFeatureNegotiationForm(form); response.setFeatureNegotiationForm(form.build());
return response; return response;
} }

View file

@ -0,0 +1,158 @@
/**
*
* Copyright 2020 Florian Schmaus
*
* 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.formtypes;
import java.util.HashMap;
import java.util.Map;
import org.jivesoftware.smack.util.Objects;
import org.jivesoftware.smackx.xdata.FormField;
import org.jivesoftware.smackx.xdata.TextSingleFormField;
import org.jivesoftware.smackx.xdata.packet.DataForm;
public class FormFieldRegistry {
private static final Map<String, Map<String, FormField.Type>> REGISTRY = new HashMap<>();
private static final Map<String, FormField.Type> LOOKASIDE_REGISTRY = new HashMap<>();
private static final Map<String, String> FIELD_NAME_TO_FORM_TYPE = new HashMap<>();
static {
register(FormField.FORM_TYPE, FormField.Type.hidden);
}
@SuppressWarnings("ReferenceEquality")
public static synchronized void register(DataForm dataForm) {
// TODO: Also allow forms of type 'result'?
if (dataForm.getType() != DataForm.Type.form) {
throw new IllegalArgumentException();
}
String formType = null;
TextSingleFormField hiddenFormTypeField = dataForm.getHiddenFormTypeField();
if (hiddenFormTypeField != null) {
formType = hiddenFormTypeField.getValue();
}
for (FormField formField : dataForm.getFields()) {
// Note that we can compare here by reference equality to skip the hidden form type field.
if (formField == hiddenFormTypeField) {
continue;
}
String fieldName = formField.getFieldName();
FormField.Type type = formField.getType();
register(formType, fieldName, type);
}
}
public static synchronized void register(String formType, String fieldName, FormField.Type type) {
if (formType == null) {
FormFieldInformation formFieldInformation = lookup(fieldName);
if (formFieldInformation != null) {
if (Objects.equals(formType, formFieldInformation.formType)
&& type.equals(formFieldInformation.formFieldType)) {
// The field is already registered, nothing to do here.
return;
}
String message = "There is already a field with the name'" + fieldName
+ "' registered with the field type '" + formFieldInformation.formFieldType
+ "', while this tries to register the field with the type '" + type + '\'';
throw new IllegalArgumentException(message);
}
LOOKASIDE_REGISTRY.put(fieldName, type);
return;
}
Map<String, FormField.Type> fieldNameToType = REGISTRY.get(formType);
if (fieldNameToType == null) {
fieldNameToType = new HashMap<>();
REGISTRY.put(formType, fieldNameToType);
} else {
FormField.Type previousType = fieldNameToType.get(fieldName);
if (previousType != null && previousType != type) {
throw new IllegalArgumentException();
}
}
fieldNameToType.put(fieldName, type);
FIELD_NAME_TO_FORM_TYPE.put(fieldName, formType);
}
public static synchronized void register(String fieldName, FormField.Type type) {
FormField.Type previousType = LOOKASIDE_REGISTRY.get(fieldName);
if (previousType != null) {
if (previousType == type) {
// Nothing to do here.
return;
}
throw new IllegalArgumentException("There is already a field with the name '" + fieldName
+ "' registered with type " + previousType
+ ", while trying to register this field with type '" + type + "'");
}
LOOKASIDE_REGISTRY.put(fieldName, type);
}
public static synchronized FormField.Type lookup(String formType, String fieldName) {
if (formType != null) {
Map<String, FormField.Type> fieldNameToTypeMap = REGISTRY.get(formType);
if (fieldNameToTypeMap != null) {
FormField.Type type = fieldNameToTypeMap.get(fieldName);
if (type != null) {
return type;
}
}
} else {
formType = FIELD_NAME_TO_FORM_TYPE.get(fieldName);
if (formType != null) {
FormField.Type type = lookup(formType, fieldName);
if (type != null) {
return type;
}
}
}
// Fallback to lookaside registry.
return LOOKASIDE_REGISTRY.get(fieldName);
}
public static synchronized FormFieldInformation lookup(String fieldName) {
String formType = FIELD_NAME_TO_FORM_TYPE.get(fieldName);
FormField.Type type = lookup(formType, fieldName);
if (type == null) {
return null;
}
return new FormFieldInformation(type, formType);
}
public static final class FormFieldInformation {
public final FormField.Type formFieldType;
public final String formType;
private FormFieldInformation(FormField.Type formFieldType, String formType) {
this.formFieldType = formFieldType;
this.formType = formType;
}
}
}

View file

@ -0,0 +1,21 @@
/**
*
* Copyright 2020 Florian Schmaus
*
* 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.
*/
/**
* Smack's implementation of XEP-0068: Field Standardization for Data Forms.
*/
package org.jivesoftware.smackx.formtypes;

View file

@ -1,6 +1,6 @@
/** /**
* *
* Copyright 2015 Florian Schmaus * Copyright 2015-2020 Florian Schmaus
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -23,17 +23,18 @@ import java.util.List;
import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NoResponseException;
import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.XMPPException.XMPPErrorException; import org.jivesoftware.smack.XMPPException.XMPPErrorException;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smackx.muc.MultiUserChatException.MucConfigurationNotSupportedException; import org.jivesoftware.smackx.muc.MultiUserChatException.MucConfigurationNotSupportedException;
import org.jivesoftware.smackx.xdata.Form;
import org.jivesoftware.smackx.xdata.FormField; import org.jivesoftware.smackx.xdata.FormField;
import org.jivesoftware.smackx.xdata.form.FillableForm;
import org.jivesoftware.smackx.xdata.form.FilledForm;
import org.jivesoftware.smackx.xdata.form.Form;
import org.jxmpp.jid.Jid; import org.jxmpp.jid.Jid;
import org.jxmpp.jid.util.JidUtil; import org.jxmpp.jid.util.JidUtil;
/** /**
* Multi-User Chat configuration form manager is used to fill out and submit a {@link Form} used to * Multi-User Chat configuration form manager is used to fill out and submit a {@link FilledForm} used to
* configure rooms. * configure rooms.
* <p> * <p>
* Room configuration needs either be done right after the room is created and still locked. Or at * Room configuration needs either be done right after the room is created and still locked. Or at
@ -43,11 +44,16 @@ import org.jxmpp.jid.util.JidUtil;
* </p> * </p>
* <p> * <p>
* The manager may not provide all possible configuration options. If you want direct access to the * The manager may not provide all possible configuration options. If you want direct access to the
* configuraiton form, use {@link MultiUserChat#getConfigurationForm()} and * configuration form, use {@link MultiUserChat#getConfigurationForm()} and
* {@link MultiUserChat#sendConfigurationForm(Form)}. * {@link MultiUserChat#sendConfigurationForm(FillableForm)}.
* </p> * </p>
*/ */
public class MucConfigFormManager { public class MucConfigFormManager {
private static final String HASH_ROOMCONFIG = "#roomconfig";
public static final String FORM_TYPE = MultiUserChatConstants.NAMESPACE + HASH_ROOMCONFIG;
/** /**
* The constant String {@value}. * The constant String {@value}.
* *
@ -73,7 +79,7 @@ public class MucConfigFormManager {
public static final String MUC_ROOMCONFIG_ROOMSECRET = "muc#roomconfig_roomsecret"; public static final String MUC_ROOMCONFIG_ROOMSECRET = "muc#roomconfig_roomsecret";
private final MultiUserChat multiUserChat; private final MultiUserChat multiUserChat;
private final Form answerForm; private final FillableForm answerForm;
private final List<Jid> owners; private final List<Jid> owners;
/** /**
@ -94,20 +100,13 @@ public class MucConfigFormManager {
// Set the answer form // Set the answer form
Form configForm = multiUserChat.getConfigurationForm(); Form configForm = multiUserChat.getConfigurationForm();
this.answerForm = configForm.createAnswerForm(); this.answerForm = configForm.getFillableForm();
// Add the default answers to the form to submit
for (FormField field : configForm.getFields()) {
if (field.getType() == FormField.Type.hidden
|| StringUtils.isNullOrEmpty(field.getVariable())) {
continue;
}
answerForm.setDefaultAnswer(field.getVariable());
}
// Set the local variables according to the fields found in the answer form // Set the local variables according to the fields found in the answer form
if (answerForm.hasField(MUC_ROOMCONFIG_ROOMOWNERS)) { FormField roomOwnersFormField = answerForm.getDataForm().getField(MUC_ROOMCONFIG_ROOMOWNERS);
if (roomOwnersFormField != null) {
// Set 'owners' to the currently configured owners // Set 'owners' to the currently configured owners
List<CharSequence> ownerStrings = answerForm.getField(MUC_ROOMCONFIG_ROOMOWNERS).getValues(); List<? extends CharSequence> ownerStrings = roomOwnersFormField.getValues();
owners = new ArrayList<>(ownerStrings.size()); owners = new ArrayList<>(ownerStrings.size());
JidUtil.jidsFrom(ownerStrings, owners, null); JidUtil.jidsFrom(ownerStrings, owners, null);
} }
@ -244,7 +243,7 @@ public class MucConfigFormManager {
} }
/** /**
* Submit the configuration as {@link Form} to the room. * Submit the configuration as {@link FilledForm} to the room.
* *
* @throws NoResponseException if there was no response from the room. * @throws NoResponseException if there was no response from the room.
* @throws XMPPErrorException if there was an XMPP error returned. * @throws XMPPErrorException if there was an XMPP error returned.

View file

@ -1,6 +1,6 @@
/** /**
* *
* Copyright 2003-2007 Jive Software. * Copyright 2003-2007 Jive Software. 2020 Florian Schmaus
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -75,8 +75,10 @@ import org.jivesoftware.smackx.muc.packet.MUCItem;
import org.jivesoftware.smackx.muc.packet.MUCOwner; import org.jivesoftware.smackx.muc.packet.MUCOwner;
import org.jivesoftware.smackx.muc.packet.MUCUser; import org.jivesoftware.smackx.muc.packet.MUCUser;
import org.jivesoftware.smackx.muc.packet.MUCUser.Status; import org.jivesoftware.smackx.muc.packet.MUCUser.Status;
import org.jivesoftware.smackx.xdata.Form;
import org.jivesoftware.smackx.xdata.FormField; import org.jivesoftware.smackx.xdata.FormField;
import org.jivesoftware.smackx.xdata.TextSingleFormField;
import org.jivesoftware.smackx.xdata.form.FillableForm;
import org.jivesoftware.smackx.xdata.form.Form;
import org.jivesoftware.smackx.xdata.packet.DataForm; import org.jivesoftware.smackx.xdata.packet.DataForm;
import org.jxmpp.jid.DomainBareJid; import org.jxmpp.jid.DomainBareJid;
@ -533,8 +535,8 @@ public class MultiUserChat {
* instant room, use {@link #makeInstant()}. * instant room, use {@link #makeInstant()}.
* <p> * <p>
* For advanced configuration options, use {@link MultiUserChat#getConfigurationForm()}, get the answer form with * For advanced configuration options, use {@link MultiUserChat#getConfigurationForm()}, get the answer form with
* {@link Form#createAnswerForm()}, fill it out and send it back to the room with * {@link Form#getFillableForm()}, fill it out and send it back to the room with
* {@link MultiUserChat#sendConfigurationForm(Form)}. * {@link MultiUserChat#sendConfigurationForm(FillableForm)}.
* </p> * </p>
*/ */
public class MucCreateConfigFormHandle { public class MucCreateConfigFormHandle {
@ -552,7 +554,7 @@ public class MultiUserChat {
*/ */
public void makeInstant() throws NoResponseException, XMPPErrorException, NotConnectedException, public void makeInstant() throws NoResponseException, XMPPErrorException, NotConnectedException,
InterruptedException { InterruptedException {
sendConfigurationForm(new Form(DataForm.Type.submit)); sendConfigurationForm(null);
} }
/** /**
@ -804,7 +806,8 @@ public class MultiUserChat {
iq.setType(IQ.Type.get); iq.setType(IQ.Type.get);
IQ answer = connection.createStanzaCollectorAndSend(iq).nextResultOrThrow(); IQ answer = connection.createStanzaCollectorAndSend(iq).nextResultOrThrow();
return Form.getFormFrom(answer); DataForm dataForm = DataForm.from(answer, MucConfigFormManager.FORM_TYPE);
return new Form(dataForm);
} }
/** /**
@ -817,11 +820,19 @@ public class MultiUserChat {
* @throws NotConnectedException if the XMPP connection is not connected. * @throws NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted. * @throws InterruptedException if the calling thread was interrupted.
*/ */
public void sendConfigurationForm(Form form) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { public void sendConfigurationForm(FillableForm form) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
final DataForm dataForm;
if (form != null) {
dataForm = form.getDataFormToSubmit();
} else {
// Instant room, cf. XEP-0045 § 10.1.2
dataForm = DataForm.builder().build();
}
MUCOwner iq = new MUCOwner(); MUCOwner iq = new MUCOwner();
iq.setTo(room); iq.setTo(room);
iq.setType(IQ.Type.set); iq.setType(IQ.Type.set);
iq.addExtension(form.getDataFormToSend()); iq.addExtension(dataForm);
connection.createStanzaCollectorAndSend(iq).nextResultOrThrow(); connection.createStanzaCollectorAndSend(iq).nextResultOrThrow();
} }
@ -849,7 +860,8 @@ public class MultiUserChat {
reg.setTo(room); reg.setTo(room);
IQ result = connection.createStanzaCollectorAndSend(reg).nextResultOrThrow(); IQ result = connection.createStanzaCollectorAndSend(reg).nextResultOrThrow();
return Form.getFormFrom(result); DataForm dataForm = DataForm.from(result);
return new Form(dataForm);
} }
/** /**
@ -869,11 +881,11 @@ public class MultiUserChat {
* @throws NotConnectedException if the XMPP connection is not connected. * @throws NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted. * @throws InterruptedException if the calling thread was interrupted.
*/ */
public void sendRegistrationForm(Form form) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { public void sendRegistrationForm(FillableForm form) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
Registration reg = new Registration(); Registration reg = new Registration();
reg.setType(IQ.Type.set); reg.setType(IQ.Type.set);
reg.setTo(room); reg.setTo(room);
reg.addExtension(form.getDataFormToSend()); reg.addExtension(form.getDataFormToSubmit());
connection.createStanzaCollectorAndSend(reg).nextResultOrThrow(); connection.createStanzaCollectorAndSend(reg).nextResultOrThrow();
} }
@ -1247,19 +1259,17 @@ public class MultiUserChat {
* @since 4.1 * @since 4.1
*/ */
public void requestVoice() throws NotConnectedException, InterruptedException { public void requestVoice() throws NotConnectedException, InterruptedException {
DataForm form = new DataForm(DataForm.Type.submit); DataForm.Builder form = DataForm.builder()
FormField.Builder formTypeField = FormField.builder(FormField.FORM_TYPE); .setFormType(MUCInitialPresence.NAMESPACE + "#request");
formTypeField.addValue(MUCInitialPresence.NAMESPACE + "#request");
form.addField(formTypeField.build()); TextSingleFormField.Builder requestVoiceField = FormField.textSingleBuilder("muc#role");
FormField.Builder requestVoiceField = FormField.builder("muc#role");
requestVoiceField.setType(FormField.Type.text_single);
requestVoiceField.setLabel("Requested role"); requestVoiceField.setLabel("Requested role");
requestVoiceField.addValue("participant"); requestVoiceField.setValue("participant");
form.addField(requestVoiceField.build()); form.addField(requestVoiceField.build());
Message message = connection.getStanzaFactory().buildMessageStanza() Message message = connection.getStanzaFactory().buildMessageStanza()
.to(room) .to(room)
.addExtension(form) .addExtension(form.build())
.build(); .build();
connection.sendStanza(message); connection.sendStanza(message);
} }

View file

@ -0,0 +1,23 @@
/**
*
* Copyright 2020 Florian Schmaus
*
* 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.muc;
public class MultiUserChatConstants {
public static final String NAMESPACE = "http://jabber.org/protocol/muc";
}

View file

@ -25,8 +25,8 @@ import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import org.jivesoftware.smackx.disco.packet.DiscoverInfo; import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
import org.jivesoftware.smackx.xdata.Form;
import org.jivesoftware.smackx.xdata.FormField; import org.jivesoftware.smackx.xdata.FormField;
import org.jivesoftware.smackx.xdata.packet.DataForm;
import org.jxmpp.jid.EntityBareJid; import org.jxmpp.jid.EntityBareJid;
import org.jxmpp.jid.Jid; import org.jxmpp.jid.Jid;
@ -130,7 +130,7 @@ public class RoomInfo {
/** /**
* The rooms extended configuration form; * The rooms extended configuration form;
*/ */
private final Form form; private final DataForm form;
RoomInfo(DiscoverInfo info) { RoomInfo(DiscoverInfo info) {
final Jid from = info.getFrom(); final Jid from = info.getFrom();
@ -166,7 +166,7 @@ public class RoomInfo {
URL logs = null; URL logs = null;
String pubsub = null; String pubsub = null;
// Get the information based on the discovered extended information // Get the information based on the discovered extended information
form = Form.getFormFrom(info); form = DataForm.from(info);
if (form != null) { if (form != null) {
FormField descField = form.getField("muc#roominfo_description"); FormField descField = form.getField("muc#roominfo_description");
if (descField != null && !descField.getValues().isEmpty()) { if (descField != null && !descField.getValues().isEmpty()) {
@ -191,7 +191,7 @@ public class RoomInfo {
FormField contactJidField = form.getField("muc#roominfo_contactjid"); FormField contactJidField = form.getField("muc#roominfo_contactjid");
if (contactJidField != null && !contactJidField.getValues().isEmpty()) { if (contactJidField != null && !contactJidField.getValues().isEmpty()) {
List<CharSequence> contactJidValues = contactJidField.getValues(); List<? extends CharSequence> contactJidValues = contactJidField.getValues();
contactJid = JidUtil.filterEntityBareJidList(JidUtil.jidSetFrom(contactJidValues)); contactJid = JidUtil.filterEntityBareJidList(JidUtil.jidSetFrom(contactJidValues));
} }
@ -420,7 +420,7 @@ public class RoomInfo {
* href="http://xmpp.org/extensions/xep-0045.html#disco-roominfo">XEP-45: * href="http://xmpp.org/extensions/xep-0045.html#disco-roominfo">XEP-45:
* Multi User Chat - 6.5 Querying for Room Information</a> * Multi User Chat - 6.5 Querying for Room Information</a>
*/ */
public Form getForm() { public DataForm getForm() {
return form; return form;
} }

View file

@ -1,6 +1,6 @@
/** /**
* *
* Copyright 2003-2007 Jive Software. * Copyright 2003-2007 Jive Software, 2020 Florian Schmaus.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -43,7 +43,7 @@ import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
import org.jivesoftware.smackx.disco.packet.DiscoverItems; import org.jivesoftware.smackx.disco.packet.DiscoverItems;
import org.jivesoftware.smackx.offline.packet.OfflineMessageInfo; import org.jivesoftware.smackx.offline.packet.OfflineMessageInfo;
import org.jivesoftware.smackx.offline.packet.OfflineMessageRequest; import org.jivesoftware.smackx.offline.packet.OfflineMessageRequest;
import org.jivesoftware.smackx.xdata.Form; import org.jivesoftware.smackx.xdata.packet.DataForm;
/** /**
* The OfflineMessageManager helps manage offline messages even before the user has sent an * The OfflineMessageManager helps manage offline messages even before the user has sent an
@ -115,13 +115,13 @@ public final class OfflineMessageManager extends Manager {
*/ */
public int getMessageCount() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { public int getMessageCount() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
DiscoverInfo info = serviceDiscoveryManager.discoverInfo(null, namespace); DiscoverInfo info = serviceDiscoveryManager.discoverInfo(null, namespace);
Form extendedInfo = Form.getFormFrom(info); DataForm dataForm = DataForm.from(info, namespace);
if (extendedInfo != null) { if (dataForm == null) {
String value = extendedInfo.getField("number_of_messages").getFirstValue();
return Integer.parseInt(value);
}
return 0; return 0;
} }
String numberOfMessagesString = dataForm.getField("number_of_messages").getFirstValue();
return Integer.parseInt(numberOfMessagesString);
}
/** /**
* Returns a List of <code>OfflineMessageHeader</code> that keep information about the * Returns a List of <code>OfflineMessageHeader</code> that keep information about the

View file

@ -22,6 +22,8 @@ import java.util.List;
import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smackx.pubsub.form.FilledConfigureForm;
/** /**
* Represents the <b>configuration</b> element of a PubSub message event which * Represents the <b>configuration</b> element of a PubSub message event which
* associates a configuration form to the node which was configured. The form * associates a configuration form to the node which was configured. The form
@ -30,18 +32,18 @@ import org.jivesoftware.smack.packet.ExtensionElement;
* @author Robin Collier * @author Robin Collier
*/ */
public class ConfigurationEvent extends NodeExtension implements EmbeddedPacketExtension { public class ConfigurationEvent extends NodeExtension implements EmbeddedPacketExtension {
private ConfigureForm form; private final FilledConfigureForm form;
public ConfigurationEvent(String nodeId) { public ConfigurationEvent(String nodeId) {
super(PubSubElementType.CONFIGURATION, nodeId); this(nodeId, null);
} }
public ConfigurationEvent(String nodeId, ConfigureForm configForm) { public ConfigurationEvent(String nodeId, FilledConfigureForm configForm) {
super(PubSubElementType.CONFIGURATION, nodeId); super(PubSubElementType.CONFIGURATION, nodeId);
form = configForm; form = configForm;
} }
public ConfigureForm getConfiguration() { public FilledConfigureForm getConfiguration() {
return form; return form;
} }
@ -50,6 +52,6 @@ public class ConfigurationEvent extends NodeExtension implements EmbeddedPacketE
if (getConfiguration() == null) if (getConfiguration() == null)
return Collections.emptyList(); return Collections.emptyList();
else else
return Arrays.asList((ExtensionElement) getConfiguration().getDataFormToSend()); return Arrays.asList((ExtensionElement) getConfiguration().getDataForm());
} }
} }

View file

@ -1,643 +0,0 @@
/**
*
* Copyright the original author or authors
*
* 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.pubsub;
import java.util.ArrayList;
import java.util.List;
import org.jivesoftware.smack.util.ParserUtils;
import org.jivesoftware.smackx.xdata.Form;
import org.jivesoftware.smackx.xdata.FormField;
import org.jivesoftware.smackx.xdata.packet.DataForm;
/**
* A decorator for a {@link Form} to easily enable reading and updating
* of node configuration. All operations read or update the underlying {@link DataForm}.
*
* <p>Unlike the {@link Form}.setAnswer(XXX)} methods, which throw an exception if the field does not
* exist, all <b>ConfigureForm.setXXX</b> methods will create the field in the wrapped form
* if it does not already exist.
*
* @author Robin Collier
*/
public class ConfigureForm extends Form {
/**
* Create a decorator from an existing {@link DataForm} that has been
* retrieved from parsing a node configuration request.
*
* @param configDataForm TODO javadoc me please
*/
public ConfigureForm(DataForm configDataForm) {
super(configDataForm);
}
/**
* Create a decorator from an existing {@link Form} for node configuration.
* Typically, this can be used to create a decorator for an answer form
* by using the result of {@link #createAnswerForm()} as the input parameter.
*
* @param nodeConfigForm TODO javadoc me please
*/
public ConfigureForm(Form nodeConfigForm) {
super(nodeConfigForm.getDataFormToSend());
}
/**
* Create a new form for configuring a node. This would typically only be used
* when creating and configuring a node at the same time via {@link PubSubManager#createNode(String, Form)}, since
* configuration of an existing node is typically accomplished by calling {@link LeafNode#getNodeConfiguration()} and
* using the resulting form to create a answer form. See {@link #ConfigureForm(Form)}.
* @param formType TODO javadoc me please
*/
public ConfigureForm(DataForm.Type formType) {
super(formType);
}
/**
* Get the currently configured {@link AccessModel}, null if it is not set.
*
* @return The current {@link AccessModel}
*/
public AccessModel getAccessModel() {
String value = getFieldValue(ConfigureNodeFields.access_model);
if (value == null)
return null;
else
return AccessModel.valueOf(value);
}
/**
* Sets the value of access model.
*
* @param accessModel TODO javadoc me please
*/
public void setAccessModel(AccessModel accessModel) {
addField(ConfigureNodeFields.access_model, FormField.Type.list_single);
setAnswer(ConfigureNodeFields.access_model.getFieldName(), getListSingle(accessModel.toString()));
}
/**
* Returns the URL of an XSL transformation which can be applied to payloads in order to
* generate an appropriate message body element.
*
* @return URL to an XSL
*/
public String getBodyXSLT() {
return getFieldValue(ConfigureNodeFields.body_xslt);
}
/**
* Set the URL of an XSL transformation which can be applied to payloads in order to
* generate an appropriate message body element.
*
* @param bodyXslt The URL of an XSL
*/
public void setBodyXSLT(String bodyXslt) {
addField(ConfigureNodeFields.body_xslt, FormField.Type.text_single);
setAnswer(ConfigureNodeFields.body_xslt.getFieldName(), bodyXslt);
}
/**
* The id's of the child nodes associated with a collection node (both leaf and collection).
*
* @return list of child nodes.
*/
public List<String> getChildren() {
return getFieldValues(ConfigureNodeFields.children);
}
/**
* Set the list of child node ids that are associated with a collection node.
*
* @param children TODO javadoc me please
*/
public void setChildren(List<String> children) {
addField(ConfigureNodeFields.children, FormField.Type.text_multi);
setAnswer(ConfigureNodeFields.children.getFieldName(), children);
}
/**
* Returns the policy that determines who may associate children with the node.
*
* @return The current policy
*/
public ChildrenAssociationPolicy getChildrenAssociationPolicy() {
String value = getFieldValue(ConfigureNodeFields.children_association_policy);
if (value == null)
return null;
else
return ChildrenAssociationPolicy.valueOf(value);
}
/**
* Sets the policy that determines who may associate children with the node.
*
* @param policy The policy being set
*/
public void setChildrenAssociationPolicy(ChildrenAssociationPolicy policy) {
addField(ConfigureNodeFields.children_association_policy, FormField.Type.list_single);
List<String> values = new ArrayList<>(1);
values.add(policy.toString());
setAnswer(ConfigureNodeFields.children_association_policy.getFieldName(), values);
}
/**
* List of JID's that are on the whitelist that determines who can associate child nodes
* with the collection node. This is only relevant if {@link #getChildrenAssociationPolicy()} is set to
* {@link ChildrenAssociationPolicy#whitelist}.
*
* @return List of the whitelist
*/
public List<String> getChildrenAssociationWhitelist() {
return getFieldValues(ConfigureNodeFields.children_association_whitelist);
}
/**
* Set the JID's in the whitelist of users that can associate child nodes with the collection
* node. This is only relevant if {@link #getChildrenAssociationPolicy()} is set to
* {@link ChildrenAssociationPolicy#whitelist}.
*
* @param whitelist The list of JID's
*/
public void setChildrenAssociationWhitelist(List<String> whitelist) {
addField(ConfigureNodeFields.children_association_whitelist, FormField.Type.jid_multi);
setAnswer(ConfigureNodeFields.children_association_whitelist.getFieldName(), whitelist);
}
/**
* Gets the maximum number of child nodes that can be associated with the collection node.
*
* @return The maximum number of child nodes
*/
public int getChildrenMax() {
return Integer.parseInt(getFieldValue(ConfigureNodeFields.children_max));
}
/**
* Set the maximum number of child nodes that can be associated with a collection node.
*
* @param max The maximum number of child nodes.
*/
public void setChildrenMax(int max) {
addField(ConfigureNodeFields.children_max, FormField.Type.text_single);
setAnswer(ConfigureNodeFields.children_max.getFieldName(), max);
}
/**
* Gets the collection node which the node is affiliated with.
*
* @return The collection node id
*/
public String getCollection() {
return getFieldValue(ConfigureNodeFields.collection);
}
/**
* Sets the collection node which the node is affiliated with.
*
* @param collection The node id of the collection node
*/
public void setCollection(String collection) {
addField(ConfigureNodeFields.collection, FormField.Type.text_single);
setAnswer(ConfigureNodeFields.collection.getFieldName(), collection);
}
/**
* Gets the URL of an XSL transformation which can be applied to the payload
* format in order to generate a valid Data Forms result that the client could
* display using a generic Data Forms rendering engine.
*
* @return The URL of an XSL transformation
*/
public String getDataformXSLT() {
return getFieldValue(ConfigureNodeFields.dataform_xslt);
}
/**
* Sets the URL of an XSL transformation which can be applied to the payload
* format in order to generate a valid Data Forms result that the client could
* display using a generic Data Forms rendering engine.
*
* @param url The URL of an XSL transformation
*/
public void setDataformXSLT(String url) {
addField(ConfigureNodeFields.dataform_xslt, FormField.Type.text_single);
setAnswer(ConfigureNodeFields.dataform_xslt.getFieldName(), url);
}
/**
* Does the node deliver payloads with event notifications.
*
* @return true if it does, false otherwise
*/
public boolean isDeliverPayloads() {
return ParserUtils.parseXmlBoolean(getFieldValue(ConfigureNodeFields.deliver_payloads));
}
/**
* Sets whether the node will deliver payloads with event notifications.
*
* @param deliver true if the payload will be delivered, false otherwise
*/
public void setDeliverPayloads(boolean deliver) {
addField(ConfigureNodeFields.deliver_payloads, FormField.Type.bool);
setAnswer(ConfigureNodeFields.deliver_payloads.getFieldName(), deliver);
}
/**
* Determines who should get replies to items.
*
* @return Who should get the reply
*/
public ItemReply getItemReply() {
String value = getFieldValue(ConfigureNodeFields.itemreply);
if (value == null)
return null;
else
return ItemReply.valueOf(value);
}
/**
* Sets who should get the replies to items.
*
* @param reply Defines who should get the reply
*/
public void setItemReply(ItemReply reply) {
addField(ConfigureNodeFields.itemreply, FormField.Type.list_single);
setAnswer(ConfigureNodeFields.itemreply.getFieldName(), getListSingle(reply.toString()));
}
/**
* Gets the maximum number of items to persisted to this node if {@link #isPersistItems()} is
* true.
*
* @return The maximum number of items to persist
*/
public int getMaxItems() {
return Integer.parseInt(getFieldValue(ConfigureNodeFields.max_items));
}
/**
* Set the maximum number of items to persisted to this node if {@link #isPersistItems()} is
* true.
*
* @param max The maximum number of items to persist
*/
public void setMaxItems(int max) {
addField(ConfigureNodeFields.max_items, FormField.Type.text_single);
setAnswer(ConfigureNodeFields.max_items.getFieldName(), max);
}
/**
* Gets the maximum payload size in bytes.
*
* @return The maximum payload size
*/
public int getMaxPayloadSize() {
return Integer.parseInt(getFieldValue(ConfigureNodeFields.max_payload_size));
}
/**
* Sets the maximum payload size in bytes.
*
* @param max The maximum payload size
*/
public void setMaxPayloadSize(int max) {
addField(ConfigureNodeFields.max_payload_size, FormField.Type.text_single);
setAnswer(ConfigureNodeFields.max_payload_size.getFieldName(), max);
}
/**
* Gets the node type.
*
* @return The node type
*/
public NodeType getNodeType() {
String value = getFieldValue(ConfigureNodeFields.node_type);
if (value == null)
return null;
else
return NodeType.valueOf(value);
}
/**
* Sets the node type.
*
* @param type The node type
*/
public void setNodeType(NodeType type) {
addField(ConfigureNodeFields.node_type, FormField.Type.list_single);
setAnswer(ConfigureNodeFields.node_type.getFieldName(), getListSingle(type.toString()));
}
/**
* Determines if subscribers should be notified when the configuration changes.
*
* @return true if they should be notified, false otherwise
*/
public boolean isNotifyConfig() {
return ParserUtils.parseXmlBoolean(getFieldValue(ConfigureNodeFields.notify_config));
}
/**
* Sets whether subscribers should be notified when the configuration changes.
*
* @param notify true if subscribers should be notified, false otherwise
*/
public void setNotifyConfig(boolean notify) {
addField(ConfigureNodeFields.notify_config, FormField.Type.bool);
setAnswer(ConfigureNodeFields.notify_config.getFieldName(), notify);
}
/**
* Determines whether subscribers should be notified when the node is deleted.
*
* @return true if subscribers should be notified, false otherwise
*/
public boolean isNotifyDelete() {
return ParserUtils.parseXmlBoolean(getFieldValue(ConfigureNodeFields.notify_delete));
}
/**
* Sets whether subscribers should be notified when the node is deleted.
*
* @param notify true if subscribers should be notified, false otherwise
*/
public void setNotifyDelete(boolean notify) {
addField(ConfigureNodeFields.notify_delete, FormField.Type.bool);
setAnswer(ConfigureNodeFields.notify_delete.getFieldName(), notify);
}
/**
* Determines whether subscribers should be notified when items are deleted
* from the node.
*
* @return true if subscribers should be notified, false otherwise
*/
public boolean isNotifyRetract() {
return ParserUtils.parseXmlBoolean(getFieldValue(ConfigureNodeFields.notify_retract));
}
/**
* Sets whether subscribers should be notified when items are deleted
* from the node.
*
* @param notify true if subscribers should be notified, false otherwise
*/
public void setNotifyRetract(boolean notify) {
addField(ConfigureNodeFields.notify_retract, FormField.Type.bool);
setAnswer(ConfigureNodeFields.notify_retract.getFieldName(), notify);
}
/**
* Determines the type of notifications which are sent.
*
* @return NotificationType for the node configuration
* @since 4.3
*/
public NotificationType getNotificationType() {
String value = getFieldValue(ConfigureNodeFields.notification_type);
if (value == null)
return null;
return NotificationType.valueOf(value);
}
/**
* Sets the NotificationType for the node.
*
* @param notificationType The enum representing the possible options
* @since 4.3
*/
public void setNotificationType(NotificationType notificationType) {
addField(ConfigureNodeFields.notification_type, FormField.Type.list_single);
setAnswer(ConfigureNodeFields.notification_type.getFieldName(), getListSingle(notificationType.toString()));
}
/**
* Determines whether items should be persisted in the node.
*
* @return true if items are persisted
*/
public boolean isPersistItems() {
return ParserUtils.parseXmlBoolean(getFieldValue(ConfigureNodeFields.persist_items));
}
/**
* Sets whether items should be persisted in the node.
*
* @param persist true if items should be persisted, false otherwise
*/
public void setPersistentItems(boolean persist) {
addField(ConfigureNodeFields.persist_items, FormField.Type.bool);
setAnswer(ConfigureNodeFields.persist_items.getFieldName(), persist);
}
/**
* Determines whether to deliver notifications to available users only.
*
* @return true if users must be available
*/
public boolean isPresenceBasedDelivery() {
return ParserUtils.parseXmlBoolean(getFieldValue(ConfigureNodeFields.presence_based_delivery));
}
/**
* Sets whether to deliver notifications to available users only.
*
* @param presenceBased true if user must be available, false otherwise
*/
public void setPresenceBasedDelivery(boolean presenceBased) {
addField(ConfigureNodeFields.presence_based_delivery, FormField.Type.bool);
setAnswer(ConfigureNodeFields.presence_based_delivery.getFieldName(), presenceBased);
}
/**
* Gets the publishing model for the node, which determines who may publish to it.
*
* @return The publishing model
*/
public PublishModel getPublishModel() {
String value = getFieldValue(ConfigureNodeFields.publish_model);
if (value == null)
return null;
else
return PublishModel.valueOf(value);
}
/**
* Sets the publishing model for the node, which determines who may publish to it.
*
* @param publish The enum representing the possible options for the publishing model
*/
public void setPublishModel(PublishModel publish) {
addField(ConfigureNodeFields.publish_model, FormField.Type.list_single);
setAnswer(ConfigureNodeFields.publish_model.getFieldName(), getListSingle(publish.toString()));
}
/**
* Gets the roster groups that are allowed to subscribe and retrieve items.
*
* @return The roster groups
*/
public List<String> getRosterGroupsAllowed() {
return getFieldValues(ConfigureNodeFields.roster_groups_allowed);
}
/**
* Sets the roster groups that are allowed to subscribe and retrieve items.
*
* @param groups The roster groups
*/
public void setRosterGroupsAllowed(List<String> groups) {
addField(ConfigureNodeFields.roster_groups_allowed, FormField.Type.list_multi);
setAnswer(ConfigureNodeFields.roster_groups_allowed.getFieldName(), groups);
}
/**
* Determines if subscriptions are allowed.
*
* @return true if subscriptions are allowed, false otherwise
* @deprecated use {@link #isSubscribe()} instead
*/
@Deprecated
// TODO: Remove in Smack 4.5.
public boolean isSubscibe() {
return isSubscribe();
}
/**
* Determines if subscriptions are allowed.
*
* @return true if subscriptions are allowed, false otherwise
*/
public boolean isSubscribe() {
return ParserUtils.parseXmlBoolean(getFieldValue(ConfigureNodeFields.subscribe));
}
/**
* Sets whether subscriptions are allowed.
*
* @param subscribe true if they are, false otherwise
*/
public void setSubscribe(boolean subscribe) {
addField(ConfigureNodeFields.subscribe, FormField.Type.bool);
setAnswer(ConfigureNodeFields.subscribe.getFieldName(), subscribe);
}
/**
* Gets the human readable node title.
*
* @return The node title
*/
@Override
public String getTitle() {
return getFieldValue(ConfigureNodeFields.title);
}
/**
* Sets a human readable title for the node.
*
* @param title The node title
*/
@Override
public void setTitle(String title) {
addField(ConfigureNodeFields.title, FormField.Type.text_single);
setAnswer(ConfigureNodeFields.title.getFieldName(), title);
}
/**
* The type of node data, usually specified by the namespace of the payload (if any).
*
* @return The type of node data
*/
public String getDataType() {
return getFieldValue(ConfigureNodeFields.type);
}
/**
* Sets the type of node data, usually specified by the namespace of the payload (if any).
*
* @param type The type of node data
*/
public void setDataType(String type) {
addField(ConfigureNodeFields.type, FormField.Type.text_single);
setAnswer(ConfigureNodeFields.type.getFieldName(), type);
}
@Override
public String toString() {
StringBuilder result = new StringBuilder(getClass().getName() + " Content [");
for (FormField formField : getFields()) {
result.append('(');
result.append(formField.getVariable());
result.append(':');
StringBuilder valuesBuilder = new StringBuilder();
for (CharSequence value : formField.getValues()) {
if (valuesBuilder.length() > 0)
result.append(',');
valuesBuilder.append(value);
}
if (valuesBuilder.length() == 0)
valuesBuilder.append("NOT SET");
result.append(valuesBuilder);
result.append(')');
}
result.append(']');
return result.toString();
}
private String getFieldValue(ConfigureNodeFields field) {
FormField formField = getField(field.getFieldName());
return formField.getFirstValue();
}
private List<String> getFieldValues(ConfigureNodeFields field) {
FormField formField = getField(field.getFieldName());
return formField.getValuesAsString();
}
private void addField(ConfigureNodeFields nodeField, FormField.Type type) {
String fieldName = nodeField.getFieldName();
if (getField(fieldName) == null) {
FormField field = FormField.builder()
.setVariable(fieldName)
.setType(type)
.build();
addField(field);
}
}
private static List<String> getListSingle(String value) {
List<String> list = new ArrayList<>(1);
list.add(value);
return list;
}
}

View file

@ -18,12 +18,13 @@ package org.jivesoftware.smackx.pubsub;
import java.net.URL; import java.net.URL;
import org.jivesoftware.smackx.xdata.Form; import org.jivesoftware.smackx.pubsub.form.ConfigureForm;
import org.jivesoftware.smackx.xdata.form.FilledForm;
/** /**
* This enumeration represents all the fields of a node configuration form. This enumeration * This enumeration represents all the fields of a node configuration form. This enumeration
* is not required when using the {@link ConfigureForm} to configure nodes, but may be helpful * is not required when using the {@link ConfigureForm} to configure nodes, but may be helpful
* for generic UI's using only a {@link Form} for configuration. * for generic UI's using only a {@link FilledForm} for configuration.
* *
* @author Robin Collier * @author Robin Collier
*/ */

View file

@ -16,7 +16,7 @@
*/ */
package org.jivesoftware.smackx.pubsub; package org.jivesoftware.smackx.pubsub;
import org.jivesoftware.smackx.xdata.Form; import org.jivesoftware.smackx.xdata.packet.DataForm;
/** /**
* Generic stanza extension which represents any PubSub form that is * Generic stanza extension which represents any PubSub form that is
@ -27,7 +27,7 @@ import org.jivesoftware.smackx.xdata.Form;
* @author Robin Collier * @author Robin Collier
*/ */
public class FormNode extends NodeExtension { public class FormNode extends NodeExtension {
private final Form configForm; private final DataForm configForm;
/** /**
* Create a {@link FormNode} which contains the specified form. * Create a {@link FormNode} which contains the specified form.
@ -35,7 +35,7 @@ public class FormNode extends NodeExtension {
* @param formType The type of form being sent * @param formType The type of form being sent
* @param submitForm The form * @param submitForm The form
*/ */
public FormNode(FormNodeType formType, Form submitForm) { public FormNode(FormNodeType formType, DataForm submitForm) {
super(formType.getNodeElement()); super(formType.getNodeElement());
if (submitForm == null) if (submitForm == null)
@ -51,7 +51,7 @@ public class FormNode extends NodeExtension {
* @param nodeId The node the form is associated with * @param nodeId The node the form is associated with
* @param submitForm The form * @param submitForm The form
*/ */
public FormNode(FormNodeType formType, String nodeId, Form submitForm) { public FormNode(FormNodeType formType, String nodeId, DataForm submitForm) {
super(formType.getNodeElement(), nodeId); super(formType.getNodeElement(), nodeId);
if (submitForm == null) if (submitForm == null)
@ -64,7 +64,7 @@ public class FormNode extends NodeExtension {
* *
* @return The form * @return The form
*/ */
public Form getForm() { public DataForm getForm() {
return configForm; return configForm;
} }
@ -84,7 +84,7 @@ public class FormNode extends NodeExtension {
} }
else else
builder.append('>'); builder.append('>');
builder.append(configForm.getDataFormToSend().toXML()); builder.append(configForm.toXML());
builder.append("</"); builder.append("</");
builder.append(getElementName() + '>'); builder.append(getElementName() + '>');
return builder.toString(); return builder.toString();

View file

@ -19,6 +19,7 @@ package org.jivesoftware.smackx.pubsub;
import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.util.XmlStringBuilder; import org.jivesoftware.smack.util.XmlStringBuilder;
import org.jivesoftware.smackx.pubsub.form.ConfigureForm;
import org.jivesoftware.smackx.pubsub.provider.ItemProvider; import org.jivesoftware.smackx.pubsub.provider.ItemProvider;
/** /**

View file

@ -16,8 +16,10 @@
*/ */
package org.jivesoftware.smackx.pubsub; package org.jivesoftware.smackx.pubsub;
import org.jivesoftware.smackx.pubsub.form.FillableConfigureForm;
/** /**
* These are the options for the node configuration setting {@link ConfigureForm#setItemReply(ItemReply)}, * These are the options for the node configuration setting {@link FillableConfigureForm#setItemReply(ItemReply)},
* which defines who should receive replies to items. * which defines who should receive replies to items.
* *
* @author Robin Collier * @author Robin Collier

View file

@ -27,6 +27,7 @@ import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.packet.IQ.Type; import org.jivesoftware.smack.packet.IQ.Type;
import org.jivesoftware.smackx.disco.packet.DiscoverItems; import org.jivesoftware.smackx.disco.packet.DiscoverItems;
import org.jivesoftware.smackx.pubsub.form.ConfigureForm;
import org.jivesoftware.smackx.pubsub.packet.PubSub; import org.jivesoftware.smackx.pubsub.packet.PubSub;
/** /**

View file

@ -37,6 +37,10 @@ import org.jivesoftware.smackx.delay.DelayInformationManager;
import org.jivesoftware.smackx.disco.packet.DiscoverInfo; import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
import org.jivesoftware.smackx.pubsub.Affiliation.AffiliationNamespace; import org.jivesoftware.smackx.pubsub.Affiliation.AffiliationNamespace;
import org.jivesoftware.smackx.pubsub.SubscriptionsExtension.SubscriptionsNamespace; import org.jivesoftware.smackx.pubsub.SubscriptionsExtension.SubscriptionsNamespace;
import org.jivesoftware.smackx.pubsub.form.ConfigureForm;
import org.jivesoftware.smackx.pubsub.form.FillableConfigureForm;
import org.jivesoftware.smackx.pubsub.form.FillableSubscribeForm;
import org.jivesoftware.smackx.pubsub.form.SubscribeForm;
import org.jivesoftware.smackx.pubsub.listener.ItemDeleteListener; import org.jivesoftware.smackx.pubsub.listener.ItemDeleteListener;
import org.jivesoftware.smackx.pubsub.listener.ItemEventListener; import org.jivesoftware.smackx.pubsub.listener.ItemEventListener;
import org.jivesoftware.smackx.pubsub.listener.NodeConfigListener; import org.jivesoftware.smackx.pubsub.listener.NodeConfigListener;
@ -45,7 +49,7 @@ import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace;
import org.jivesoftware.smackx.pubsub.util.NodeUtils; import org.jivesoftware.smackx.pubsub.util.NodeUtils;
import org.jivesoftware.smackx.shim.packet.Header; import org.jivesoftware.smackx.shim.packet.Header;
import org.jivesoftware.smackx.shim.packet.HeadersExtension; import org.jivesoftware.smackx.shim.packet.HeadersExtension;
import org.jivesoftware.smackx.xdata.Form; import org.jivesoftware.smackx.xdata.packet.DataForm;
import org.jxmpp.jid.Jid; import org.jxmpp.jid.Jid;
import org.jxmpp.jid.impl.JidCreate; import org.jxmpp.jid.impl.JidCreate;
@ -81,7 +85,7 @@ public abstract class Node {
} }
/** /**
* Returns a configuration form, from which you can create an answer form to be submitted * Returns a configuration form, from which you can create an answer form to be submitted
* via the {@link #sendConfigurationForm(Form)}. * via the {@link #sendConfigurationForm(FillableConfigureForm)}.
* *
* @return the configuration form * @return the configuration form
* @throws XMPPErrorException if there was an XMPP error returned. * @throws XMPPErrorException if there was an XMPP error returned.
@ -97,17 +101,17 @@ public abstract class Node {
} }
/** /**
* Update the configuration with the contents of the new {@link Form}. * Update the configuration with the contents of the new {@link FillableConfigureForm}.
* *
* @param submitForm TODO javadoc me please * @param configureForm the filled node configuration form with the nodes new configuration.
* @throws XMPPErrorException if there was an XMPP error returned. * @throws XMPPErrorException if there was an XMPP error returned.
* @throws NoResponseException if there was no response from the remote entity. * @throws NoResponseException if there was no response from the remote entity.
* @throws NotConnectedException if the XMPP connection is not connected. * @throws NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted. * @throws InterruptedException if the calling thread was interrupted.
*/ */
public void sendConfigurationForm(Form submitForm) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { public void sendConfigurationForm(FillableConfigureForm configureForm) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
PubSub packet = createPubsubPacket(Type.set, new FormNode(FormNodeType.CONFIGURE_OWNER, PubSub packet = createPubsubPacket(Type.set, new FormNode(FormNodeType.CONFIGURE_OWNER,
getId(), submitForm)); getId(), configureForm.getDataFormToSubmit()));
pubSubManager.getConnection().createStanzaCollectorAndSend(packet).nextResultOrThrow(); pubSubManager.getConnection().createStanzaCollectorAndSend(packet).nextResultOrThrow();
} }
@ -454,9 +458,10 @@ public abstract class Node {
* @throws NotConnectedException if the XMPP connection is not connected. * @throws NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted. * @throws InterruptedException if the calling thread was interrupted.
*/ */
public Subscription subscribe(Jid jid, SubscribeForm subForm) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { public Subscription subscribe(Jid jid, FillableSubscribeForm subForm) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
DataForm submitForm = subForm.getDataFormToSubmit();
PubSub request = createPubsubPacket(Type.set, new SubscribeExtension(jid, getId())); PubSub request = createPubsubPacket(Type.set, new SubscribeExtension(jid, getId()));
request.addExtension(new FormNode(FormNodeType.OPTIONS, subForm)); request.addExtension(new FormNode(FormNodeType.OPTIONS, submitForm));
PubSub reply = sendPubsubPacket(request); PubSub reply = sendPubsubPacket(request);
return reply.getExtension(PubSubElementType.SUBSCRIPTION); return reply.getExtension(PubSubElementType.SUBSCRIPTION);
} }
@ -483,11 +488,11 @@ public abstract class Node {
* @throws NotConnectedException if the XMPP connection is not connected. * @throws NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted. * @throws InterruptedException if the calling thread was interrupted.
* @throws IllegalArgumentException if the provided string is not a valid JID. * @throws IllegalArgumentException if the provided string is not a valid JID.
* @deprecated use {@link #subscribe(Jid, SubscribeForm)} instead. * @deprecated use {@link #subscribe(Jid, FillableSubscribeForm)} instead.
*/ */
@Deprecated @Deprecated
// TODO: Remove in Smack 4.5. // TODO: Remove in Smack 4.5.
public Subscription subscribe(String jidString, SubscribeForm subForm) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { public Subscription subscribe(String jidString, FillableSubscribeForm subForm) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
Jid jid; Jid jid;
try { try {
jid = JidCreate.from(jidString); jid = JidCreate.from(jidString);
@ -529,7 +534,7 @@ public abstract class Node {
/** /**
* Returns a SubscribeForm for subscriptions, from which you can create an answer form to be submitted * Returns a SubscribeForm for subscriptions, from which you can create an answer form to be submitted
* via the {@link #sendConfigurationForm(Form)}. * via the {@link #sendConfigurationForm(FillableConfigureForm)}.
* *
* @param jid TODO javadoc me please * @param jid TODO javadoc me please
* *

View file

@ -16,9 +16,11 @@
*/ */
package org.jivesoftware.smackx.pubsub; package org.jivesoftware.smackx.pubsub;
import org.jivesoftware.smackx.pubsub.form.FillableConfigureForm;
/** /**
* Specify the delivery style for event notifications. Denotes possible values * Specify the delivery style for event notifications. Denotes possible values
* for {@link ConfigureForm#setNotificationType(NotificationType)}. * for {@link FillableConfigureForm#setNotificationType(NotificationType)}.
* *
* @author Timothy Pitt * @author Timothy Pitt
*/ */

View file

@ -20,6 +20,7 @@ import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.util.XmlStringBuilder; import org.jivesoftware.smack.util.XmlStringBuilder;
import org.jivesoftware.smackx.pubsub.form.ConfigureForm;
import org.jivesoftware.smackx.pubsub.provider.ItemProvider; import org.jivesoftware.smackx.pubsub.provider.ItemProvider;
/** /**

View file

@ -16,6 +16,8 @@
*/ */
package org.jivesoftware.smackx.pubsub; package org.jivesoftware.smackx.pubsub;
import org.jivesoftware.smackx.pubsub.form.SubscribeForm;
/** /**
* Defines the possible valid presence states for node subscription via * Defines the possible valid presence states for node subscription via
* {@link SubscribeForm#getShowValues()}. * {@link SubscribeForm#getShowValues()}.

View file

@ -46,11 +46,12 @@ import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
import org.jivesoftware.smackx.disco.packet.DiscoverItems; import org.jivesoftware.smackx.disco.packet.DiscoverItems;
import org.jivesoftware.smackx.pubsub.PubSubException.NotALeafNodeException; import org.jivesoftware.smackx.pubsub.PubSubException.NotALeafNodeException;
import org.jivesoftware.smackx.pubsub.PubSubException.NotAPubSubNodeException; import org.jivesoftware.smackx.pubsub.PubSubException.NotAPubSubNodeException;
import org.jivesoftware.smackx.pubsub.form.ConfigureForm;
import org.jivesoftware.smackx.pubsub.form.FillableConfigureForm;
import org.jivesoftware.smackx.pubsub.packet.PubSub; import org.jivesoftware.smackx.pubsub.packet.PubSub;
import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace; import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace;
import org.jivesoftware.smackx.pubsub.util.NodeUtils; import org.jivesoftware.smackx.pubsub.util.NodeUtils;
import org.jivesoftware.smackx.xdata.Form; import org.jivesoftware.smackx.xdata.packet.DataForm;
import org.jivesoftware.smackx.xdata.FormField;
import org.jxmpp.jid.BareJid; import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.DomainBareJid; import org.jxmpp.jid.DomainBareJid;
@ -255,16 +256,18 @@ public final class PubSubManager extends Manager {
* @throws NotConnectedException if the XMPP connection is not connected. * @throws NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted. * @throws InterruptedException if the calling thread was interrupted.
*/ */
public Node createNode(String nodeId, Form config) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { public Node createNode(String nodeId, FillableConfigureForm config) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
PubSub request = PubSub.createPubsubPacket(pubSubService, Type.set, new NodeExtension(PubSubElementType.CREATE, nodeId)); PubSub request = PubSub.createPubsubPacket(pubSubService, Type.set, new NodeExtension(PubSubElementType.CREATE, nodeId));
boolean isLeafNode = true; boolean isLeafNode = true;
if (config != null) { if (config != null) {
request.addExtension(new FormNode(FormNodeType.CONFIGURE, config)); DataForm submitForm = config.getDataFormToSubmit();
FormField nodeTypeField = config.getField(ConfigureNodeFields.node_type.getFieldName()); request.addExtension(new FormNode(FormNodeType.CONFIGURE, submitForm));
NodeType nodeType = config.getNodeType();
if (nodeTypeField != null) // Note that some implementations do to have the pubsub#node_type field in their defauilt configuration,
isLeafNode = nodeTypeField.getValues().get(0).toString().equals(NodeType.leaf.toString()); // which I believe to be a bug. However, since PubSub specifies the default node type to be 'leaf' we assume
// leaf if the field does not exist.
isLeafNode = nodeType == null || nodeType == NodeType.leaf;
} }
// Errors will cause exceptions in getReply, so it only returns // Errors will cause exceptions in getReply, so it only returns

View file

@ -16,9 +16,11 @@
*/ */
package org.jivesoftware.smackx.pubsub; package org.jivesoftware.smackx.pubsub;
import org.jivesoftware.smackx.pubsub.form.FillableConfigureForm;
/** /**
* Determines who may publish to a node. Denotes possible values * Determines who may publish to a node. Denotes possible values
* for {@link ConfigureForm#setPublishModel(PublishModel)}. * for {@link FillableConfigureForm#setPublishModel(PublishModel)}.
* *
* @author Robin Collier * @author Robin Collier
*/ */

View file

@ -1,214 +0,0 @@
/**
*
* Copyright the original author or authors
*
* 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.pubsub;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.UnknownFormatConversionException;
import org.jivesoftware.smack.util.ParserUtils;
import org.jivesoftware.smackx.xdata.Form;
import org.jivesoftware.smackx.xdata.FormField;
import org.jivesoftware.smackx.xdata.packet.DataForm;
import org.jxmpp.util.XmppDateTime;
/**
* A decorator for a {@link Form} to easily enable reading and updating
* of subscription options. All operations read or update the underlying {@link DataForm}.
*
* <p>Unlike the {@link Form}.setAnswer(XXX)} methods, which throw an exception if the field does not
* exist, all <b>SubscribeForm.setXXX</b> methods will create the field in the wrapped form
* if it does not already exist.
*
* @author Robin Collier
*/
public class SubscribeForm extends Form {
public SubscribeForm(DataForm configDataForm) {
super(configDataForm);
}
public SubscribeForm(Form subscribeOptionsForm) {
super(subscribeOptionsForm.getDataFormToSend());
}
public SubscribeForm(DataForm.Type formType) {
super(formType);
}
/**
* Determines if an entity wants to receive notifications.
*
* @return true if want to receive, false otherwise
*/
public boolean isDeliverOn() {
return ParserUtils.parseXmlBoolean(getFieldValue(SubscribeOptionFields.deliver));
}
/**
* Sets whether an entity wants to receive notifications.
*
* @param deliverNotifications TODO javadoc me please
*/
public void setDeliverOn(boolean deliverNotifications) {
addField(SubscribeOptionFields.deliver, FormField.Type.bool);
setAnswer(SubscribeOptionFields.deliver.getFieldName(), deliverNotifications);
}
/**
* Determines if notifications should be delivered as aggregations or not.
*
* @return true to aggregate, false otherwise
*/
public boolean isDigestOn() {
return ParserUtils.parseXmlBoolean(getFieldValue(SubscribeOptionFields.digest));
}
/**
* Sets whether notifications should be delivered as aggregations or not.
*
* @param digestOn true to aggregate, false otherwise
*/
public void setDigestOn(boolean digestOn) {
addField(SubscribeOptionFields.deliver, FormField.Type.bool);
setAnswer(SubscribeOptionFields.deliver.getFieldName(), digestOn);
}
/**
* Gets the minimum number of milliseconds between sending notification digests.
*
* @return The frequency in milliseconds
*/
public int getDigestFrequency() {
return Integer.parseInt(getFieldValue(SubscribeOptionFields.digest_frequency));
}
/**
* Sets the minimum number of milliseconds between sending notification digests.
*
* @param frequency The frequency in milliseconds
*/
public void setDigestFrequency(int frequency) {
addField(SubscribeOptionFields.digest_frequency, FormField.Type.text_single);
setAnswer(SubscribeOptionFields.digest_frequency.getFieldName(), frequency);
}
/**
* Get the time at which the leased subscription will expire, or has expired.
*
* @return The expiry date
*/
public Date getExpiry() {
String dateTime = getFieldValue(SubscribeOptionFields.expire);
try {
return XmppDateTime.parseDate(dateTime);
}
catch (ParseException e) {
UnknownFormatConversionException exc = new UnknownFormatConversionException(dateTime);
exc.initCause(e);
throw exc;
}
}
/**
* Sets the time at which the leased subscription will expire, or has expired.
*
* @param expire The expiry date
*/
public void setExpiry(Date expire) {
addField(SubscribeOptionFields.expire, FormField.Type.text_single);
setAnswer(SubscribeOptionFields.expire.getFieldName(), XmppDateTime.formatXEP0082Date(expire));
}
/**
* Determines whether the entity wants to receive an XMPP message body in
* addition to the payload format.
*
* @return true to receive the message body, false otherwise
*/
public boolean isIncludeBody() {
return ParserUtils.parseXmlBoolean(getFieldValue(SubscribeOptionFields.include_body));
}
/**
* Sets whether the entity wants to receive an XMPP message body in
* addition to the payload format.
*
* @param include true to receive the message body, false otherwise
*/
public void setIncludeBody(boolean include) {
addField(SubscribeOptionFields.include_body, FormField.Type.bool);
setAnswer(SubscribeOptionFields.include_body.getFieldName(), include);
}
/**
* Gets the {@link PresenceState} for which an entity wants to receive
* notifications.
*
* @return the list of states
*/
public List<PresenceState> getShowValues() {
ArrayList<PresenceState> result = new ArrayList<>(5);
for (String state : getFieldValues(SubscribeOptionFields.show_values)) {
result.add(PresenceState.valueOf(state));
}
return result;
}
/**
* Sets the list of {@link PresenceState} for which an entity wants
* to receive notifications.
*
* @param stateValues The list of states
*/
public void setShowValues(Collection<PresenceState> stateValues) {
ArrayList<String> values = new ArrayList<>(stateValues.size());
for (PresenceState state : stateValues) {
values.add(state.toString());
}
addField(SubscribeOptionFields.show_values, FormField.Type.list_multi);
setAnswer(SubscribeOptionFields.show_values.getFieldName(), values);
}
private String getFieldValue(SubscribeOptionFields field) {
FormField formField = getField(field.getFieldName());
return formField.getFirstValue();
}
private List<String> getFieldValues(SubscribeOptionFields field) {
FormField formField = getField(field.getFieldName());
return formField.getValuesAsString();
}
private void addField(SubscribeOptionFields nodeField, FormField.Type type) {
String fieldName = nodeField.getFieldName();
if (getField(fieldName) == null) {
FormField.Builder field = FormField.builder(fieldName);
field.setType(type);
addField(field.build());
}
}
}

View file

@ -0,0 +1,33 @@
/**
*
* Copyright 2020 Florian Schmaus
*
* 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.pubsub.form;
import org.jivesoftware.smackx.xdata.form.Form;
import org.jivesoftware.smackx.xdata.packet.DataForm;
public class ConfigureForm extends Form implements ConfigureFormReader {
public ConfigureForm(DataForm dataForm) {
super(dataForm);
ensureFormType(dataForm, FORM_TYPE);
}
@Override
public FillableConfigureForm getFillableForm() {
return new FillableConfigureForm(getDataForm());
}
}

View file

@ -0,0 +1,292 @@
/**
*
* Copyright the original author or authors, 2020 Florian Schmaus
*
* 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.pubsub.form;
import java.util.Collections;
import java.util.List;
import org.jivesoftware.smackx.pubsub.AccessModel;
import org.jivesoftware.smackx.pubsub.ChildrenAssociationPolicy;
import org.jivesoftware.smackx.pubsub.ConfigureNodeFields;
import org.jivesoftware.smackx.pubsub.ItemReply;
import org.jivesoftware.smackx.pubsub.NodeType;
import org.jivesoftware.smackx.pubsub.NotificationType;
import org.jivesoftware.smackx.pubsub.PublishModel;
import org.jivesoftware.smackx.pubsub.packet.PubSub;
import org.jivesoftware.smackx.xdata.FormField;
import org.jivesoftware.smackx.xdata.JidMultiFormField;
import org.jivesoftware.smackx.xdata.form.FormReader;
import org.jxmpp.jid.Jid;
public interface ConfigureFormReader extends FormReader {
String FORM_TYPE = PubSub.NAMESPACE + "#node_config";
/**
* Get the currently configured {@link AccessModel}, null if it is not set.
*
* @return The current {@link AccessModel}
*/
default AccessModel getAccessModel() {
String value = readFirstValue(ConfigureNodeFields.access_model.getFieldName());
if (value == null) {
return null;
}
return AccessModel.valueOf(value);
}
/**
* Returns the URL of an XSL transformation which can be applied to payloads in order to
* generate an appropriate message body element.
*
* @return URL to an XSL
*/
default String getBodyXSLT() {
return readFirstValue(ConfigureNodeFields.body_xslt.getFieldName());
}
/**
* The id's of the child nodes associated with a collection node (both leaf and collection).
*
* @return list of child nodes.
*/
default List<String> getChildren() {
return readStringValues(ConfigureNodeFields.children.getFieldName());
}
/**
* Returns the policy that determines who may associate children with the node.
*
* @return The current policy
*/
default ChildrenAssociationPolicy getChildrenAssociationPolicy() {
String value = readFirstValue(ConfigureNodeFields.children_association_policy.getFieldName());
if (value == null) {
return null;
}
return ChildrenAssociationPolicy.valueOf(value);
}
/**
* List of JID's that are on the whitelist that determines who can associate child nodes
* with the collection node. This is only relevant if {@link #getChildrenAssociationPolicy()} is set to
* {@link ChildrenAssociationPolicy#whitelist}.
*
* @return List of the whitelist
*/
default List<Jid> getChildrenAssociationWhitelist() {
FormField formField = read(ConfigureNodeFields.children_association_whitelist.getFieldName());
if (formField == null) {
Collections.emptyList();
}
JidMultiFormField jidMultiFormField = formField.ifPossibleAs(JidMultiFormField.class);
return jidMultiFormField.getValues();
}
/**
* Gets the maximum number of child nodes that can be associated with the collection node.
*
* @return The maximum number of child nodes
*/
default Integer getChildrenMax() {
return readInteger(ConfigureNodeFields.children_max.getFieldName());
}
/**
* Gets the collection node which the node is affiliated with.
*
* @return The collection node id
*/
default List<? extends CharSequence> getCollection() {
return readValues(ConfigureNodeFields.collection.getFieldName());
}
/**
* Gets the URL of an XSL transformation which can be applied to the payload
* format in order to generate a valid Data Forms result that the client could
* display using a generic Data Forms rendering engine.
*
* @return The URL of an XSL transformation
*/
default String getDataformXSLT() {
return readFirstValue(ConfigureNodeFields.dataform_xslt.getFieldName());
}
/**
* Does the node deliver payloads with event notifications.
*
* @return true if it does, false otherwise
*/
default Boolean isDeliverPayloads() {
return readBoolean(ConfigureNodeFields.deliver_payloads.getFieldName());
}
/**
* Determines who should get replies to items.
*
* @return Who should get the reply
*/
default ItemReply getItemReply() {
String value = readFirstValue(ConfigureNodeFields.itemreply.getFieldName());
if (value == null) {
return null;
}
return ItemReply.valueOf(value);
}
/**
* Gets the maximum number of items to persisted to this node if {@link #isPersistItems()} is
* true.
*
* @return The maximum number of items to persist
*/
default Integer getMaxItems() {
return readInteger(ConfigureNodeFields.max_items.getFieldName());
}
/**
* Gets the maximum payload size in bytes.
*
* @return The maximum payload size
*/
default Integer getMaxPayloadSize() {
return readInteger(ConfigureNodeFields.max_payload_size.getFieldName());
}
/**
* Gets the node type.
*
* @return The node type
*/
default NodeType getNodeType() {
String value = readFirstValue(ConfigureNodeFields.node_type.getFieldName());
if (value == null) {
return null;
}
return NodeType.valueOf(value);
}
/**
* Determines if subscribers should be notified when the configuration changes.
*
* @return true if they should be notified, false otherwise
*/
default Boolean isNotifyConfig() {
return readBoolean(ConfigureNodeFields.notify_config.getFieldName());
}
/**
* Determines whether subscribers should be notified when the node is deleted.
*
* @return true if subscribers should be notified, false otherwise
*/
default Boolean isNotifyDelete() {
return readBoolean(ConfigureNodeFields.notify_delete.getFieldName());
}
/**
* Determines whether subscribers should be notified when items are deleted
* from the node.
*
* @return true if subscribers should be notified, false otherwise
*/
default Boolean isNotifyRetract() {
return readBoolean(ConfigureNodeFields.notify_retract.getFieldName());
}
/**
* Determines the type of notifications which are sent.
*
* @return NotificationType for the node configuration
* @since 4.3
*/
default NotificationType getNotificationType() {
String value = readFirstValue(ConfigureNodeFields.notification_type.getFieldName());
if (value == null) {
return null;
}
return NotificationType.valueOf(value);
}
/**
* Determines whether items should be persisted in the node.
*
* @return true if items are persisted
*/
default boolean isPersistItems() {
return readBoolean(ConfigureNodeFields.persist_items.getFieldName());
}
/**
* Determines whether to deliver notifications to available users only.
*
* @return true if users must be available
*/
default boolean isPresenceBasedDelivery() {
return readBoolean(ConfigureNodeFields.presence_based_delivery.getFieldName());
}
/**
* Gets the publishing model for the node, which determines who may publish to it.
*
* @return The publishing model
*/
default PublishModel getPublishModel() {
String value = readFirstValue(ConfigureNodeFields.publish_model.getFieldName());
if (value == null) {
return null;
}
return PublishModel.valueOf(value);
}
/**
* Gets the roster groups that are allowed to subscribe and retrieve items.
*
* @return The roster groups
*/
default List<String> getRosterGroupsAllowed() {
return readStringValues(ConfigureNodeFields.roster_groups_allowed.getFieldName());
}
/**
* Determines if subscriptions are allowed.
*
* @return true if subscriptions are allowed, false otherwise
*/
default boolean isSubscribe() {
return readBoolean(ConfigureNodeFields.subscribe.getFieldName());
}
/**
* Gets the human readable node title.
*
* @return The node title
*/
default String getTitle() {
return readFirstValue(ConfigureNodeFields.title.getFieldName());
}
/**
* The type of node data, usually specified by the namespace of the payload (if any).
*
* @return The type of node data
*/
default String getDataType() {
return readFirstValue(ConfigureNodeFields.type.getFieldName());
}
}

View file

@ -0,0 +1,314 @@
/**
*
* Copyright the original author or authors, 2020 Florian Schmaus
*
* 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.pubsub.form;
import java.net.URL;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.jivesoftware.smackx.pubsub.AccessModel;
import org.jivesoftware.smackx.pubsub.ChildrenAssociationPolicy;
import org.jivesoftware.smackx.pubsub.ConfigureNodeFields;
import org.jivesoftware.smackx.pubsub.ItemReply;
import org.jivesoftware.smackx.pubsub.NodeType;
import org.jivesoftware.smackx.pubsub.NotificationType;
import org.jivesoftware.smackx.pubsub.PublishModel;
import org.jivesoftware.smackx.xdata.FormField;
import org.jivesoftware.smackx.xdata.form.FillableForm;
import org.jivesoftware.smackx.xdata.packet.DataForm;
import org.jxmpp.jid.Jid;
public class FillableConfigureForm extends FillableForm implements ConfigureFormReader {
public FillableConfigureForm(DataForm dataForm) {
super(dataForm);
}
/**
* Sets the value of access model.
*
* @param accessModel TODO javadoc me please
*/
public void setAccessModel(AccessModel accessModel) {
FormField formField = FormField.listSingleBuilder(ConfigureNodeFields.access_model.getFieldName())
.setValue(accessModel)
.build();
write(formField);
}
/**
* Set the URL of an XSL transformation which can be applied to payloads in order to
* generate an appropriate message body element.
*
* @param bodyXslt The URL of an XSL
*/
public void setBodyXSLT(String bodyXslt) {
FormField formField = FormField.listSingleBuilder(ConfigureNodeFields.body_xslt.getFieldName())
.setValue(bodyXslt)
.build();
write(formField);
}
/**
* Set the list of child node ids that are associated with a collection node.
*
* @param children TODO javadoc me please
*/
public void setChildren(List<String> children) {
FormField formField = FormField.textMultiBuilder(ConfigureNodeFields.children.getFieldName())
.addValues(children)
.build();
write(formField);
}
/**
* Sets the policy that determines who may associate children with the node.
*
* @param policy The policy being set
*/
public void setChildrenAssociationPolicy(ChildrenAssociationPolicy policy) {
FormField formField = FormField.listSingleBuilder(ConfigureNodeFields.children_association_policy.getFieldName())
.setValue(policy)
.build();
write(formField);
}
/**
* Set the JID's in the whitelist of users that can associate child nodes with the collection
* node. This is only relevant if {@link #getChildrenAssociationPolicy()} is set to
* {@link ChildrenAssociationPolicy#whitelist}.
*
* @param whitelist The list of JID's
*/
public void setChildrenAssociationWhitelist(List<? extends Jid> whitelist) {
FormField formField = FormField.jidMultiBuilder(ConfigureNodeFields.children_association_whitelist.getFieldName())
.addValues(whitelist)
.build();
write(formField);
}
/**
* Set the maximum number of child nodes that can be associated with a collection node.
*
* @param max The maximum number of child nodes.
*/
public void setChildrenMax(int max) {
FormField formField = FormField.textSingleBuilder(ConfigureNodeFields.children_max.getFieldName())
.setValue(max)
.build();
write(formField);
}
/**
* Sets the collection node which the node is affiliated with.
*
* @param collection The node id of the collection node
*/
public void setCollection(String collection) {
setCollections(Collections.singletonList(collection));
}
public void setCollections(Collection<String> collections) {
FormField formField = FormField.textMultiBuilder(ConfigureNodeFields.collection.getFieldName())
.addValues(collections)
.build();
write(formField);
}
/**
* Sets the URL of an XSL transformation which can be applied to the payload
* format in order to generate a valid Data Forms result that the client could
* display using a generic Data Forms rendering engine.
*
* @param url The URL of an XSL transformation
*/
public void setDataformXSLT(URL url) {
FormField formField = FormField.textSingleBuilder(ConfigureNodeFields.dataform_xslt.getFieldName())
.setValue(url)
.build();
write(formField);
}
/**
* Sets whether the node will deliver payloads with event notifications.
*
* @param deliver true if the payload will be delivered, false otherwise
*/
public void setDeliverPayloads(boolean deliver) {
writeBoolean(ConfigureNodeFields.deliver_payloads.getFieldName(), deliver);
}
/**
* Sets who should get the replies to items.
*
* @param reply Defines who should get the reply
*/
public void setItemReply(ItemReply reply) {
FormField formField = FormField.listSingleBuilder(ConfigureNodeFields.itemreply.getFieldName())
.setValue(reply)
.build();
write(formField);
}
/**
* Set the maximum number of items to persisted to this node if {@link #isPersistItems()} is
* true.
*
* @param max The maximum number of items to persist
*/
public void setMaxItems(int max) {
FormField formField = FormField.textSingleBuilder(ConfigureNodeFields.max_items.getFieldName())
.setValue(max)
.build();
write(formField);
}
/**
* Sets the maximum payload size in bytes.
*
* @param max The maximum payload size
*/
public void setMaxPayloadSize(int max) {
FormField formField = FormField.textSingleBuilder(ConfigureNodeFields.max_payload_size.getFieldName())
.setValue(max)
.build();
write(formField);
}
/**
* Sets the node type.
*
* @param type The node type
*/
public void setNodeType(NodeType type) {
FormField formField = FormField.listSingleBuilder(ConfigureNodeFields.node_type.getFieldName())
.setValue(type)
.build();
write(formField);
}
/**
* Sets whether subscribers should be notified when the configuration changes.
*
* @param notify true if subscribers should be notified, false otherwise
*/
public void setNotifyConfig(boolean notify) {
writeBoolean(ConfigureNodeFields.notify_config.getFieldName(), notify);
}
/**
* Sets whether subscribers should be notified when the node is deleted.
*
* @param notify true if subscribers should be notified, false otherwise
*/
public void setNotifyDelete(boolean notify) {
writeBoolean(ConfigureNodeFields.notify_delete.getFieldName(), notify);
}
/**
* Sets whether subscribers should be notified when items are deleted
* from the node.
*
* @param notify true if subscribers should be notified, false otherwise
*/
public void setNotifyRetract(boolean notify) {
writeBoolean(ConfigureNodeFields.notify_retract.getFieldName(), notify);
}
/**
* Sets the NotificationType for the node.
*
* @param notificationType The enum representing the possible options
* @since 4.3
*/
public void setNotificationType(NotificationType notificationType) {
FormField formField = FormField.listSingleBuilder(ConfigureNodeFields.notification_type.getFieldName())
.setValue(notificationType)
.build();
write(formField);
}
/**
* Sets whether items should be persisted in the node.
*
* @param persist true if items should be persisted, false otherwise
*/
public void setPersistentItems(boolean persist) {
writeBoolean(ConfigureNodeFields.persist_items.getFieldName(), persist);
}
/**
* Sets whether to deliver notifications to available users only.
*
* @param presenceBased true if user must be available, false otherwise
*/
public void setPresenceBasedDelivery(boolean presenceBased) {
writeBoolean(ConfigureNodeFields.presence_based_delivery.getFieldName(), presenceBased);
}
/**
* Sets the publishing model for the node, which determines who may publish to it.
*
* @param publish The enum representing the possible options for the publishing model
*/
public void setPublishModel(PublishModel publish) {
FormField formField = FormField.listSingleBuilder(ConfigureNodeFields.publish_model.getFieldName())
.setValue(publish)
.build();
write(formField);
}
/**
* Sets the roster groups that are allowed to subscribe and retrieve items.
*
* @param groups The roster groups
*/
public void setRosterGroupsAllowed(List<? extends CharSequence> groups) {
writeListMulti(ConfigureNodeFields.roster_groups_allowed.getFieldName(), groups);
}
/**
* Sets whether subscriptions are allowed.
*
* @param subscribe true if they are, false otherwise
*/
public void setSubscribe(boolean subscribe) {
writeBoolean(ConfigureNodeFields.subscribe.getFieldName(), subscribe);
}
/**
* Sets a human readable title for the node.
*
* @param title The node title
*/
public void setTitle(CharSequence title) {
writeTextSingle(ConfigureNodeFields.title.getFieldName(), title);
}
/**
* Sets the type of node data, usually specified by the namespace of the payload (if any).
*
* @param type The type of node data
*/
public void setDataType(CharSequence type) {
writeTextSingle(ConfigureNodeFields.type.getFieldName(), type);
}
}

View file

@ -0,0 +1,95 @@
/**
*
* Copyright 2020 Florian Schmaus
*
* 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.pubsub.form;
import java.util.Collection;
import java.util.Date;
import org.jivesoftware.smackx.pubsub.PresenceState;
import org.jivesoftware.smackx.pubsub.SubscribeOptionFields;
import org.jivesoftware.smackx.xdata.FormField;
import org.jivesoftware.smackx.xdata.ListMultiFormField;
import org.jivesoftware.smackx.xdata.form.FillableForm;
import org.jivesoftware.smackx.xdata.packet.DataForm;
public class FillableSubscribeForm extends FillableForm {
public FillableSubscribeForm(DataForm dataForm) {
super(dataForm);
}
/**
* Sets whether an entity wants to receive notifications.
*
* @param deliverNotifications TODO javadoc me please
*/
public void setDeliverOn(boolean deliverNotifications) {
writeBoolean(SubscribeOptionFields.deliver.getFieldName(), deliverNotifications);
}
/**
* Sets whether notifications should be delivered as aggregations or not.
*
* @param digestOn true to aggregate, false otherwise
*/
public void setDigestOn(boolean digestOn) {
writeBoolean(SubscribeOptionFields.digest.getFieldName(), digestOn);
}
/**
* Sets the minimum number of milliseconds between sending notification digests.
*
* @param frequency The frequency in milliseconds
*/
public void setDigestFrequency(int frequency) {
write(SubscribeOptionFields.digest_frequency.getFieldName(), frequency);
}
/**
* Sets the time at which the leased subscription will expire, or has expired.
*
* @param expire The expiry date
*/
public void setExpiry(Date expire) {
write(SubscribeOptionFields.expire.getFieldName(), expire);
}
/**
* Sets whether the entity wants to receive an XMPP message body in
* addition to the payload format.
*
* @param include true to receive the message body, false otherwise
*/
public void setIncludeBody(boolean include) {
writeBoolean(SubscribeOptionFields.include_body.getFieldName(), include);
}
/**
* Sets the list of {@link PresenceState} for which an entity wants
* to receive notifications.
*
* @param stateValues The list of states
*/
public void setShowValues(Collection<PresenceState> stateValues) {
ListMultiFormField.Builder builder = FormField.listMultiBuilder(SubscribeOptionFields.show_values.getFieldName());
for (PresenceState state : stateValues) {
builder.addValue(state.toString());
}
write(builder.build());
}
}

View file

@ -0,0 +1,29 @@
/**
*
* Copyright 2020 Florian Schmaus
*
* 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.pubsub.form;
import org.jivesoftware.smackx.xdata.form.FilledForm;
import org.jivesoftware.smackx.xdata.packet.DataForm;
public class FilledConfigureForm extends FilledForm implements ConfigureFormReader {
public FilledConfigureForm(DataForm dataForm) {
super(dataForm);
ensureFormType(dataForm, FORM_TYPE);
}
}

View file

@ -0,0 +1,29 @@
/**
*
* Copyright 2020 Florian Schmaus
*
* 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.pubsub.form;
import org.jivesoftware.smackx.xdata.form.FilledForm;
import org.jivesoftware.smackx.xdata.packet.DataForm;
public class FilledSubscribeForm extends FilledForm implements SubscribeFormReader {
public FilledSubscribeForm(DataForm dataForm) {
super(dataForm);
ensureFormType(dataForm, FORM_TYPE);
}
}

View file

@ -0,0 +1,29 @@
/**
*
* Copyright 2020 Florian Schmaus
*
* 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.pubsub.form;
import org.jivesoftware.smackx.xdata.form.Form;
import org.jivesoftware.smackx.xdata.packet.DataForm;
public class SubscribeForm extends Form implements SubscribeFormReader {
public SubscribeForm(DataForm dataForm) {
super(dataForm);
ensureFormType(dataForm, FORM_TYPE);
}
}

View file

@ -0,0 +1,95 @@
/**
*
* Copyright 2020 Florian Schmaus
*
* 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.pubsub.form;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.jivesoftware.smackx.pubsub.PresenceState;
import org.jivesoftware.smackx.pubsub.SubscribeOptionFields;
import org.jivesoftware.smackx.pubsub.packet.PubSub;
import org.jivesoftware.smackx.xdata.form.FormReader;
public interface SubscribeFormReader extends FormReader {
String FORM_TYPE = PubSub.NAMESPACE + "#subscribe_options";
/**
* Determines if an entity wants to receive notifications.
*
* @return true if want to receive, false otherwise
*/
default boolean isDeliverOn() {
return readBoolean(SubscribeOptionFields.deliver.getFieldName());
}
/**
* Determines if notifications should be delivered as aggregations or not.
*
* @return true to aggregate, false otherwise
*/
default Boolean isDigestOn() {
return readBoolean(SubscribeOptionFields.digest.getFieldName());
}
/**
* Gets the minimum number of milliseconds between sending notification digests.
*
* @return The frequency in milliseconds
*/
default Integer getDigestFrequency() {
return readInteger(SubscribeOptionFields.digest_frequency.getFieldName());
}
/**
* Get the time at which the leased subscription will expire, or has expired.
*
* @return The expiry date
* @throws ParseException in case the date could not be parsed.
*/
default Date getExpiry() throws ParseException {
return readDate(SubscribeOptionFields.expire.getFieldName());
}
/**
* Determines whether the entity wants to receive an XMPP message body in
* addition to the payload format.
*
* @return true to receive the message body, false otherwise
*/
default Boolean isIncludeBody() {
return readBoolean(SubscribeOptionFields.include_body.getFieldName());
}
/**
* Gets the {@link PresenceState} for which an entity wants to receive
* notifications.
*
* @return the list of states
*/
default List<PresenceState> getShowValues() {
List<String> values = readStringValues(SubscribeOptionFields.show_values.getFieldName());
List<PresenceState> result = new ArrayList<>(values.size());
for (String state : values) {
result.add(PresenceState.valueOf(state));
}
return result;
}
}

View file

@ -0,0 +1,21 @@
/**
*
* Copyright 2020 Florian Schmaus
*
* 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.
*/
/**
* Smack's implementation of Data Forms (XEP-0004) for PubSub.
*/
package org.jivesoftware.smackx.pubsub.form;

View file

@ -23,7 +23,7 @@ import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.provider.EmbeddedExtensionProvider; import org.jivesoftware.smack.provider.EmbeddedExtensionProvider;
import org.jivesoftware.smackx.pubsub.ConfigurationEvent; import org.jivesoftware.smackx.pubsub.ConfigurationEvent;
import org.jivesoftware.smackx.pubsub.ConfigureForm; import org.jivesoftware.smackx.pubsub.form.FilledConfigureForm;
import org.jivesoftware.smackx.xdata.packet.DataForm; import org.jivesoftware.smackx.xdata.packet.DataForm;
/** /**
@ -38,6 +38,6 @@ public class ConfigEventProvider extends EmbeddedExtensionProvider<Configuration
if (content.size() == 0) if (content.size() == 0)
return new ConfigurationEvent(attMap.get("node")); return new ConfigurationEvent(attMap.get("node"));
else else
return new ConfigurationEvent(attMap.get("node"), new ConfigureForm((DataForm) content.iterator().next())); return new ConfigurationEvent(attMap.get("node"), new FilledConfigureForm((DataForm) content.iterator().next()));
} }
} }

View file

@ -24,7 +24,6 @@ import org.jivesoftware.smack.provider.EmbeddedExtensionProvider;
import org.jivesoftware.smackx.pubsub.FormNode; import org.jivesoftware.smackx.pubsub.FormNode;
import org.jivesoftware.smackx.pubsub.FormNodeType; import org.jivesoftware.smackx.pubsub.FormNodeType;
import org.jivesoftware.smackx.xdata.Form;
import org.jivesoftware.smackx.xdata.packet.DataForm; import org.jivesoftware.smackx.xdata.packet.DataForm;
/** /**
@ -36,6 +35,6 @@ import org.jivesoftware.smackx.xdata.packet.DataForm;
public class FormNodeProvider extends EmbeddedExtensionProvider<FormNode> { public class FormNodeProvider extends EmbeddedExtensionProvider<FormNode> {
@Override @Override
protected FormNode createReturnExtension(String currentElement, String currentNamespace, Map<String, String> attributeMap, List<? extends ExtensionElement> content) { protected FormNode createReturnExtension(String currentElement, String currentNamespace, Map<String, String> attributeMap, List<? extends ExtensionElement> content) {
return new FormNode(FormNodeType.valueOfFromElementName(currentElement, currentNamespace), attributeMap.get("node"), new Form((DataForm) content.iterator().next())); return new FormNode(FormNodeType.valueOfFromElementName(currentElement, currentNamespace), attributeMap.get("node"), (DataForm) content.iterator().next());
} }
} }

View file

@ -18,10 +18,10 @@ package org.jivesoftware.smackx.pubsub.util;
import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smackx.pubsub.ConfigureForm;
import org.jivesoftware.smackx.pubsub.FormNode; import org.jivesoftware.smackx.pubsub.FormNode;
import org.jivesoftware.smackx.pubsub.PubSubElementType; import org.jivesoftware.smackx.pubsub.PubSubElementType;
import org.jivesoftware.smackx.xdata.Form; import org.jivesoftware.smackx.pubsub.form.ConfigureForm;
import org.jivesoftware.smackx.xdata.packet.DataForm;
/** /**
* Utility for extracting information from packets. * Utility for extracting information from packets.
@ -38,7 +38,7 @@ public class NodeUtils {
*/ */
public static ConfigureForm getFormFromPacket(Stanza packet, PubSubElementType elem) { public static ConfigureForm getFormFromPacket(Stanza packet, PubSubElementType elem) {
FormNode config = (FormNode) packet.getExtensionElement(elem.getElementName(), elem.getNamespace().getXmlns()); FormNode config = (FormNode) packet.getExtensionElement(elem.getElementName(), elem.getNamespace().getXmlns());
Form formReply = config.getForm(); DataForm dataForm = config.getForm();
return new ConfigureForm(formReply); return new ConfigureForm(dataForm);
} }
} }

View file

@ -66,7 +66,7 @@ public class ReportedData {
private ReportedData(DataForm dataForm) { private ReportedData(DataForm dataForm) {
// Add the columns to the report based on the reported data fields // Add the columns to the report based on the reported data fields
for (FormField field : dataForm.getReportedData().getFields()) { for (FormField field : dataForm.getReportedData().getFields()) {
columns.add(new Column(field.getLabel(), field.getVariable(), field.getType())); columns.add(new Column(field.getLabel(), field.getFieldName(), field.getType()));
} }
// Add the rows to the report based on the form's items // Add the rows to the report based on the form's items
@ -76,7 +76,7 @@ public class ReportedData {
// The field is created with all the values of the data form's field // The field is created with all the values of the data form's field
List<CharSequence> values = new ArrayList<>(); List<CharSequence> values = new ArrayList<>();
values.addAll(field.getValues()); values.addAll(field.getValues());
fieldList.add(new Field(field.getVariable(), values)); fieldList.add(new Field(field.getFieldName(), values));
} }
rows.add(new Row(fieldList)); rows.add(new Row(fieldList));
} }

View file

@ -24,8 +24,8 @@ import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.xml.XmlPullParser; import org.jivesoftware.smack.xml.XmlPullParser;
import org.jivesoftware.smack.xml.XmlPullParserException; import org.jivesoftware.smack.xml.XmlPullParserException;
import org.jivesoftware.smackx.xdata.Form;
import org.jivesoftware.smackx.xdata.FormField; import org.jivesoftware.smackx.xdata.FormField;
import org.jivesoftware.smackx.xdata.packet.DataForm;
/** /**
* SimpleUserSearch is used to support the non-dataform type of XEP 55. This provides * SimpleUserSearch is used to support the non-dataform type of XEP 55. This provides
@ -39,14 +39,14 @@ class SimpleUserSearch extends IQ {
public static final String ELEMENT = UserSearch.ELEMENT; public static final String ELEMENT = UserSearch.ELEMENT;
public static final String NAMESPACE = UserSearch.NAMESPACE; public static final String NAMESPACE = UserSearch.NAMESPACE;
private Form form; private DataForm form;
private ReportedData data; private ReportedData data;
SimpleUserSearch() { SimpleUserSearch() {
super(ELEMENT, NAMESPACE); super(ELEMENT, NAMESPACE);
} }
public void setForm(Form form) { public void setForm(DataForm form) {
this.form = form; this.form = form;
} }
@ -65,7 +65,7 @@ class SimpleUserSearch extends IQ {
StringBuilder buf = new StringBuilder(); StringBuilder buf = new StringBuilder();
if (form == null) { if (form == null) {
form = Form.getFormFrom(this); form = DataForm.from(this);
} }
if (form == null) { if (form == null) {
@ -73,7 +73,7 @@ class SimpleUserSearch extends IQ {
} }
for (FormField field : form.getFields()) { for (FormField field : form.getFields()) {
String name = field.getVariable(); String name = field.getFieldName();
String value = getSingleValue(field); String value = getSingleValue(field);
if (value.trim().length() > 0) { if (value.trim().length() > 0) {
buf.append('<').append(name).append('>').append(value).append("</").append(name).append('>'); buf.append('<').append(name).append('>').append(value).append("</").append(name).append('>');

View file

@ -31,8 +31,6 @@ import org.jivesoftware.smack.util.PacketParserUtils;
import org.jivesoftware.smack.xml.XmlPullParser; import org.jivesoftware.smack.xml.XmlPullParser;
import org.jivesoftware.smack.xml.XmlPullParserException; import org.jivesoftware.smack.xml.XmlPullParserException;
import org.jivesoftware.smackx.xdata.Form;
import org.jivesoftware.smackx.xdata.FormField;
import org.jivesoftware.smackx.xdata.packet.DataForm; import org.jivesoftware.smackx.xdata.packet.DataForm;
import org.jxmpp.jid.DomainBareJid; import org.jxmpp.jid.DomainBareJid;
@ -70,13 +68,13 @@ public class UserSearch extends SimpleIQ {
* @throws NotConnectedException if the XMPP connection is not connected. * @throws NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted. * @throws InterruptedException if the calling thread was interrupted.
*/ */
public Form getSearchForm(XMPPConnection con, DomainBareJid searchService) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { public DataForm getSearchForm(XMPPConnection con, DomainBareJid searchService) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
UserSearch search = new UserSearch(); UserSearch search = new UserSearch();
search.setType(IQ.Type.get); search.setType(IQ.Type.get);
search.setTo(searchService); search.setTo(searchService);
IQ response = con.createStanzaCollectorAndSend(search).nextResultOrThrow(); IQ response = con.createStanzaCollectorAndSend(search).nextResultOrThrow();
return Form.getFormFrom(response); return DataForm.from(response, NAMESPACE);
} }
/** /**
@ -91,11 +89,11 @@ public class UserSearch extends SimpleIQ {
* @throws NotConnectedException if the XMPP connection is not connected. * @throws NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted. * @throws InterruptedException if the calling thread was interrupted.
*/ */
public ReportedData sendSearchForm(XMPPConnection con, Form searchForm, DomainBareJid searchService) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { public ReportedData sendSearchForm(XMPPConnection con, DataForm searchForm, DomainBareJid searchService) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
UserSearch search = new UserSearch(); UserSearch search = new UserSearch();
search.setType(IQ.Type.set); search.setType(IQ.Type.set);
search.setTo(searchService); search.setTo(searchService);
search.addExtension(searchForm.getDataFormToSend()); search.addExtension(searchForm);
IQ response = con.createStanzaCollectorAndSend(search).nextResultOrThrow(); IQ response = con.createStanzaCollectorAndSend(search).nextResultOrThrow();
return ReportedData.getReportedDataFrom(response); return ReportedData.getReportedDataFrom(response);
@ -113,7 +111,7 @@ public class UserSearch extends SimpleIQ {
* @throws NotConnectedException if the XMPP connection is not connected. * @throws NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted. * @throws InterruptedException if the calling thread was interrupted.
*/ */
public ReportedData sendSimpleSearchForm(XMPPConnection con, Form searchForm, DomainBareJid searchService) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { public ReportedData sendSimpleSearchForm(XMPPConnection con, DataForm searchForm, DomainBareJid searchService) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
SimpleUserSearch search = new SimpleUserSearch(); SimpleUserSearch search = new SimpleUserSearch();
search.setForm(searchForm); search.setForm(searchForm);
search.setType(IQ.Type.set); search.setType(IQ.Type.set);
@ -137,11 +135,7 @@ public class UserSearch extends SimpleIQ {
boolean done = false; boolean done = false;
while (!done) { while (!done) {
XmlPullParser.Event eventType = parser.next(); XmlPullParser.Event eventType = parser.next();
if (eventType == XmlPullParser.Event.START_ELEMENT && parser.getName().equals("instructions")) { if (eventType == XmlPullParser.Event.START_ELEMENT && parser.getName().equals("item")) {
buildDataForm(simpleUserSearch, parser.nextText(), parser, xmlEnvironment);
return simpleUserSearch;
}
else if (eventType == XmlPullParser.Event.START_ELEMENT && parser.getName().equals("item")) {
simpleUserSearch.parseItems(parser); simpleUserSearch.parseItems(parser);
return simpleUserSearch; return simpleUserSearch;
} }
@ -164,50 +158,4 @@ public class UserSearch extends SimpleIQ {
} }
} }
private static void buildDataForm(SimpleUserSearch search,
String instructions, XmlPullParser parser, XmlEnvironment xmlEnvironment) throws XmlPullParserException, IOException, SmackParsingException {
DataForm dataForm = new DataForm(DataForm.Type.form);
boolean done = false;
dataForm.setTitle("User Search");
dataForm.addInstruction(instructions);
while (!done) {
XmlPullParser.Event eventType = parser.next();
if (eventType == XmlPullParser.Event.START_ELEMENT && !parser.getNamespace().equals("jabber:x:data")) {
String name = parser.getName();
FormField.Builder field = FormField.builder(name);
// Handle hard coded values.
if (name.equals("first")) {
field.setLabel("First Name");
}
else if (name.equals("last")) {
field.setLabel("Last Name");
}
else if (name.equals("email")) {
field.setLabel("Email Address");
}
else if (name.equals("nick")) {
field.setLabel("Nickname");
}
field.setType(FormField.Type.text_single);
dataForm.addField(field.build());
}
else if (eventType == XmlPullParser.Event.END_ELEMENT) {
if (parser.getName().equals("query")) {
done = true;
}
}
else if (eventType == XmlPullParser.Event.START_ELEMENT && parser.getNamespace().equals("jabber:x:data")) {
PacketParserUtils.addExtensionElement(search, parser, xmlEnvironment);
done = true;
}
}
if (search.getExtension(DataForm.class) == null) {
search.addExtension(dataForm);
}
}
} }

View file

@ -24,7 +24,7 @@ import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException.XMPPErrorException; import org.jivesoftware.smack.XMPPException.XMPPErrorException;
import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
import org.jivesoftware.smackx.xdata.Form; import org.jivesoftware.smackx.xdata.packet.DataForm;
import org.jxmpp.jid.DomainBareJid; import org.jxmpp.jid.DomainBareJid;
@ -71,7 +71,7 @@ public class UserSearchManager {
* @throws NotConnectedException if the XMPP connection is not connected. * @throws NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted. * @throws InterruptedException if the calling thread was interrupted.
*/ */
public Form getSearchForm(DomainBareJid searchService) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { public DataForm getSearchForm(DomainBareJid searchService) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
return userSearch.getSearchForm(con, searchService); return userSearch.getSearchForm(con, searchService);
} }
@ -87,7 +87,7 @@ public class UserSearchManager {
* @throws NotConnectedException if the XMPP connection is not connected. * @throws NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted. * @throws InterruptedException if the calling thread was interrupted.
*/ */
public ReportedData getSearchResults(Form searchForm, DomainBareJid searchService) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { public ReportedData getSearchResults(DataForm searchForm, DomainBareJid searchService) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
return userSearch.sendSearchForm(con, searchForm, searchService); return userSearch.sendSearchForm(con, searchForm, searchService);
} }

View file

@ -0,0 +1,92 @@
/**
*
* Copyright 2020 Florian Schmaus
*
* 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.xdata;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import org.jivesoftware.smack.util.CollectionUtil;
import org.jxmpp.util.XmppDateTime;
public class AbstractMultiFormField extends FormField {
private final List<String> values;
protected AbstractMultiFormField(Builder<?, ?> builder) {
super(builder);
values = CollectionUtil.cloneAndSeal(builder.values);
}
@Override
public final List<String> getValues() {
return values;
}
public abstract static class Builder<F extends FormField, B extends FormField.Builder<F, B>>
extends FormField.Builder<F, B> {
private List<String> values;
protected Builder(AbstractMultiFormField formField) {
super(formField);
values = CollectionUtil.newListWith(formField.getValues());
}
protected Builder(String fieldName, FormField.Type type) {
super(fieldName, type);
}
private void ensureValuesAreInitialized() {
if (values == null) {
values = new ArrayList<>();
}
}
@Override
protected void resetInternal() {
values = null;
}
public abstract B addValue(CharSequence value);
public B addValueVerbatim(CharSequence value) {
ensureValuesAreInitialized();
values.add(value.toString());
return getThis();
}
public final B addValue(Date date) {
String dateString = XmppDateTime.formatXEP0082Date(date);
return addValueVerbatim(dateString);
}
public final B addValues(Collection<? extends CharSequence> values) {
ensureValuesAreInitialized();
for (CharSequence value : values) {
this.values.add(value.toString());
}
return getThis();
}
}
}

View file

@ -0,0 +1,100 @@
/**
*
* Copyright 2020 Florian Schmaus.
*
* 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.xdata;
import java.net.URL;
import java.util.Date;
import org.jxmpp.util.XmppDateTime;
public class AbstractSingleStringValueFormField extends SingleValueFormField {
private final String value;
protected AbstractSingleStringValueFormField(Builder<?, ?> builder) {
super(builder);
value = builder.value;
}
@Override
public final String getValue() {
return value;
}
public final Integer getValueAsInt() {
if (value == null) {
return null;
}
return Integer.valueOf(value);
}
public abstract static class Builder<F extends FormField, B extends FormField.Builder<F, B>> extends FormField.Builder<F, B> {
private String value;
protected Builder(AbstractSingleStringValueFormField abstractSingleFormField) {
super(abstractSingleFormField);
value = abstractSingleFormField.getValue();
}
protected Builder(String fieldName, FormField.Type type) {
super(fieldName, type);
}
@Override
protected void resetInternal() {
value = null;
}
/**
* Set the value.
*
* @param value the value to set.
* @return a reference to this builder.
* @deprecated use {@link #setValue(CharSequence)} instead.
*/
@Deprecated
// TODO: Remove in Smack 4.6.
public B addValue(CharSequence value) {
return setValue(value);
}
public B setValue(CharSequence value) {
this.value = value.toString();
return getThis();
}
public B setValue(Enum<?> value) {
this.value = value.toString();
return getThis();
}
public B setValue(int value) {
this.value = Integer.toString(value);
return getThis();
}
public B setValue(URL value) {
return setValue(value.toString());
}
public B setValue(Date date) {
String dateString = XmppDateTime.formatXEP0082Date(date);
return setValue(dateString);
}
}
}

View file

@ -0,0 +1,98 @@
/**
*
* Copyright 2020 Florian Schmaus.
*
* 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.xdata;
import org.jivesoftware.smack.util.ParserUtils;
public class BooleanFormField extends SingleValueFormField {
private final Boolean value;
protected BooleanFormField(Builder builder) {
super(builder);
value = builder.value;
}
@Override
public String getValue() {
if (value == null) {
return null;
}
return value.toString();
}
public Boolean getValueAsBoolean() {
return value;
}
public Builder asBuilder() {
return new Builder(this);
}
public static final class Builder extends FormField.Builder<BooleanFormField, BooleanFormField.Builder> {
private Boolean value;
private Builder(BooleanFormField booleanFormField) {
super(booleanFormField);
value = booleanFormField.value;
}
Builder(String fieldName) {
super(fieldName, FormField.Type.bool);
}
@Override
protected void resetInternal() {
value = null;
}
/**
* Set the value.
*
* @param value the value to set.
* @return a reference to this builder.
* @deprecated use {@link #setValue(CharSequence)} instead.
*/
@Deprecated
// TODO: Remove in Smack 4.6.
public Builder addValue(CharSequence value) {
return setValue(value);
}
public Builder setValue(CharSequence value) {
boolean valueBoolean = ParserUtils.parseXmlBoolean(value.toString());
return setValue(valueBoolean);
}
public Builder setValue(boolean value) {
this.value = value;
return this;
}
@Override
public BooleanFormField build() {
return new BooleanFormField(this);
}
@Override
public Builder getThis() {
return this;
}
}
}

View file

@ -1,517 +0,0 @@
/**
*
* Copyright 2003-2007 Jive Software.
*
* 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.xdata;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;
import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smackx.xdata.packet.DataForm;
/**
* Represents a Form for gathering data. The form could be of the following types:
* <ul>
* <li>form &rarr; Indicates a form to fill out.</li>
* <li>submit &rarr; The form is filled out, and this is the data that is being returned from
* the form.</li>
* <li>cancel &rarr; The form was cancelled. Tell the asker that piece of information.</li>
* <li>result &rarr; Data results being returned from a search, or some other query.</li>
* </ul>
*
* Depending of the form's type different operations are available. For example, it's only possible
* to set answers if the form is of type "submit".
*
* @see <a href="http://xmpp.org/extensions/xep-0004.html">XEP-0004 Data Forms</a>
*
* @author Gaston Dombiak
*/
public class Form {
private DataForm dataForm;
/**
* Returns a new ReportedData if the stanza is used for gathering data and includes an
* extension that matches the elementName and namespace "x","jabber:x:data".
*
* @param packet the stanza used for gathering data.
* @return the data form parsed from the stanza or <code>null</code> if there was not
* a form in the packet.
*/
public static Form getFormFrom(Stanza packet) {
// Check if the packet includes the DataForm extension
DataForm dataForm = DataForm.from(packet);
if (dataForm != null) {
if (dataForm.getReportedData() == null)
return new Form(dataForm);
}
// Otherwise return null
return null;
}
/**
* Creates a new Form that will wrap an existing DataForm. The wrapped DataForm must be
* used for gathering data.
*
* @param dataForm the data form used for gathering data.
*/
public Form(DataForm dataForm) {
this.dataForm = dataForm;
}
/**
* Creates a new Form of a given type from scratch.
*
* @param type the form's type (e.g. form, submit, cancel, result).
*/
public Form(DataForm.Type type) {
this.dataForm = new DataForm(type);
}
/**
* Adds a new field to complete as part of the form.
*
* @param field the field to complete.
*/
public void addField(FormField field) {
dataForm.addField(field);
}
/**
* Sets a new String value to a given form's field. The field whose variable matches the
* requested variable will be completed with the specified value. If no field could be found
* for the specified variable then an exception will be raised.<p>
*
* If the value to set to the field is not a basic type (e.g. String, boolean, int, etc.) you
* can use this message where the String value is the String representation of the object.
*
* @param variable the variable name that was completed.
* @param value the String value that was answered.
* @throws IllegalStateException if the form is not of type "submit".
* @throws IllegalArgumentException if the form does not include the specified variable or
* if the answer type does not correspond with the field type..
*/
public void setAnswer(String variable, CharSequence value) {
FormField field = getField(variable);
if (field == null) {
throw new IllegalArgumentException("Field not found for the specified variable name.");
}
switch (field.getType()) {
case text_multi:
case text_private:
case text_single:
case jid_single:
case hidden:
break;
default:
throw new IllegalArgumentException("This field is not of type String.");
}
setAnswer(field, value);
}
/**
* Sets a new int value to a given form's field. The field whose variable matches the
* requested variable will be completed with the specified value. If no field could be found
* for the specified variable then an exception will be raised.
*
* @param variable the variable name that was completed.
* @param value the int value that was answered.
* @throws IllegalStateException if the form is not of type "submit".
* @throws IllegalArgumentException if the form does not include the specified variable or
* if the answer type does not correspond with the field type.
*/
public void setAnswer(String variable, int value) {
FormField field = getField(variable);
if (field == null) {
throw new IllegalArgumentException("Field not found for the specified variable name.");
}
validateThatFieldIsText(field);
setAnswer(field, value);
}
/**
* Sets a new long value to a given form's field. The field whose variable matches the
* requested variable will be completed with the specified value. If no field could be found
* for the specified variable then an exception will be raised.
*
* @param variable the variable name that was completed.
* @param value the long value that was answered.
* @throws IllegalStateException if the form is not of type "submit".
* @throws IllegalArgumentException if the form does not include the specified variable or
* if the answer type does not correspond with the field type.
*/
public void setAnswer(String variable, long value) {
FormField field = getField(variable);
if (field == null) {
throw new IllegalArgumentException("Field not found for the specified variable name.");
}
validateThatFieldIsText(field);
setAnswer(field, value);
}
/**
* Sets a new float value to a given form's field. The field whose variable matches the
* requested variable will be completed with the specified value. If no field could be found
* for the specified variable then an exception will be raised.
*
* @param variable the variable name that was completed.
* @param value the float value that was answered.
* @throws IllegalStateException if the form is not of type "submit".
* @throws IllegalArgumentException if the form does not include the specified variable or
* if the answer type does not correspond with the field type.
*/
public void setAnswer(String variable, float value) {
FormField field = getField(variable);
if (field == null) {
throw new IllegalArgumentException("Field not found for the specified variable name.");
}
validateThatFieldIsText(field);
setAnswer(field, value);
}
/**
* Sets a new double value to a given form's field. The field whose variable matches the
* requested variable will be completed with the specified value. If no field could be found
* for the specified variable then an exception will be raised.
*
* @param variable the variable name that was completed.
* @param value the double value that was answered.
* @throws IllegalStateException if the form is not of type "submit".
* @throws IllegalArgumentException if the form does not include the specified variable or
* if the answer type does not correspond with the field type.
*/
public void setAnswer(String variable, double value) {
FormField field = getField(variable);
if (field == null) {
throw new IllegalArgumentException("Field not found for the specified variable name.");
}
validateThatFieldIsText(field);
setAnswer(field, value);
}
private static void validateThatFieldIsText(FormField field) {
switch (field.getType()) {
case text_multi:
case text_private:
case text_single:
break;
default:
throw new IllegalArgumentException("This field is not of type text (multi, private or single).");
}
}
/**
* Sets a new boolean value to a given form's field. The field whose variable matches the
* requested variable will be completed with the specified value. If no field could be found
* for the specified variable then an exception will be raised.
*
* @param variable the variable name that was completed.
* @param value the boolean value that was answered.
* @throws IllegalStateException if the form is not of type "submit".
* @throws IllegalArgumentException if the form does not include the specified variable or
* if the answer type does not correspond with the field type.
*/
public void setAnswer(String variable, boolean value) {
FormField field = getField(variable);
if (field == null) {
throw new IllegalArgumentException("Field not found for the specified variable name.");
}
if (field.getType() != FormField.Type.bool) {
throw new IllegalArgumentException("This field is not of type boolean.");
}
setAnswer(field, Boolean.toString(value));
}
/**
* Sets a new Object value to a given form's field. In fact, the object representation
* (i.e. #toString) will be the actual value of the field.<p>
*
* If the value to set to the field is not a basic type (e.g. String, boolean, int, etc.) you
* will need to use {@link #setAnswer(String, String)} where the String value is the
* String representation of the object.<p>
*
* Before setting the new value to the field we will check if the form is of type submit. If
* the form isn't of type submit means that it's not possible to complete the form and an
* exception will be thrown.
*
* @param field the form field that was completed.
* @param value the Object value that was answered. The object representation will be the
* actual value.
* @throws IllegalStateException if the form is not of type "submit".
*/
private void setAnswer(FormField field, Object value) {
if (!isSubmitType()) {
throw new IllegalStateException("Cannot set an answer if the form is not of type " +
"\"submit\"");
}
FormField filledOutfield = field.buildAnswer().addValue(value.toString()).build();
dataForm.replaceField(filledOutfield);
}
/**
* Sets a new values to a given form's field. The field whose variable matches the requested
* variable will be completed with the specified values. If no field could be found for
* the specified variable then an exception will be raised.<p>
*
* The Objects contained in the List could be of any type. The String representation of them
* (i.e. #toString) will be actually used when sending the answer to the server.
*
* @param variable the variable that was completed.
* @param values the values that were answered.
* @throws IllegalStateException if the form is not of type "submit".
* @throws IllegalArgumentException if the form does not include the specified variable.
*/
public void setAnswer(String variable, Collection<? extends CharSequence> values) {
if (!isSubmitType()) {
throw new IllegalStateException("Cannot set an answer if the form is not of type " +
"\"submit\"");
}
FormField field = getField(variable);
if (field != null) {
// Check that the field can accept a collection of values
switch (field.getType()) {
case jid_multi:
case list_multi:
case list_single:
case text_multi:
case hidden:
break;
default:
throw new IllegalArgumentException("This field only accept list of values.");
}
FormField filledOutfield = field.buildAnswer().addValues(values).build();
dataForm.replaceField(filledOutfield);
}
else {
throw new IllegalArgumentException("Couldn't find a field for the specified variable.");
}
}
/**
* Sets the default value as the value of a given form's field. The field whose variable matches
* the requested variable will be completed with its default value. If no field could be found
* for the specified variable then an exception will be raised.
*
* @param variable the variable to complete with its default value.
* @throws IllegalStateException if the form is not of type "submit".
* @throws IllegalArgumentException if the form does not include the specified variable.
*/
public void setDefaultAnswer(String variable) {
if (!isSubmitType()) {
throw new IllegalStateException("Cannot set an answer if the form is not of type " +
"\"submit\"");
}
FormField field = getField(variable);
if (field != null) {
FormField.Builder filledOutFormFieldBuilder = field.buildAnswer();
// Set the default value
for (CharSequence value : field.getValues()) {
filledOutFormFieldBuilder.addValue(value);
}
dataForm.replaceField(filledOutFormFieldBuilder.build());
}
else {
throw new IllegalArgumentException("Couldn't find a field for the specified variable.");
}
}
/**
* Returns a List of the fields that are part of the form.
*
* @return a List of the fields that are part of the form.
*/
public List<FormField> getFields() {
return dataForm.getFields();
}
/**
* Returns the field of the form whose variable matches the specified variable.
* The fields of type FIXED will never be returned since they do not specify a
* variable.
*
* @param variable the variable to look for in the form fields.
* @return the field of the form whose variable matches the specified variable.
*/
public FormField getField(String variable) {
return dataForm.getField(variable);
}
/**
* Check if a field with the given variable exists.
*
* @param variable the variable to check for.
* @return true if a field with the variable exists, false otherwise.
* @since 4.2
*/
public boolean hasField(String variable) {
return dataForm.hasField(variable);
}
/**
* Returns the instructions that explain how to fill out the form and what the form is about.
*
* @return instructions that explain how to fill out the form.
*/
public String getInstructions() {
StringBuilder sb = new StringBuilder();
// Join the list of instructions together separated by newlines
for (Iterator<String> it = dataForm.getInstructions().iterator(); it.hasNext();) {
sb.append(it.next());
// If this is not the last instruction then append a newline
if (it.hasNext()) {
sb.append('\n');
}
}
return sb.toString();
}
/**
* Returns the description of the data. It is similar to the title on a web page or an X
* window. You can put a title on either a form to fill out, or a set of data results.
*
* @return description of the data.
*/
public String getTitle() {
return dataForm.getTitle();
}
/**
* Returns the meaning of the data within the context. The data could be part of a form
* to fill out, a form submission or data results.
*
* @return the form's type.
*/
public DataForm.Type getType() {
return dataForm.getType();
}
/**
* Sets instructions that explain how to fill out the form and what the form is about.
*
* @param instructions instructions that explain how to fill out the form.
*/
public void setInstructions(String instructions) {
// Split the instructions into multiple instructions for each existent newline
ArrayList<String> instructionsList = new ArrayList<>();
StringTokenizer st = new StringTokenizer(instructions, "\n");
while (st.hasMoreTokens()) {
instructionsList.add(st.nextToken());
}
// Set the new list of instructions
dataForm.setInstructions(instructionsList);
}
/**
* Sets the description of the data. It is similar to the title on a web page or an X window.
* You can put a title on either a form to fill out, or a set of data results.
*
* @param title description of the data.
*/
public void setTitle(String title) {
dataForm.setTitle(title);
}
/**
* Returns a DataForm that serves to send this Form to the server. If the form is of type
* submit, it may contain fields with no value. These fields will be removed since they only
* exist to assist the user while editing/completing the form in a UI.
*
* @return the wrapped DataForm.
*/
public DataForm getDataFormToSend() {
if (isSubmitType()) {
// Create a new DataForm that contains only the answered fields
DataForm dataFormToSend = new DataForm(getType());
for (FormField field : getFields()) {
if (!field.getValues().isEmpty()) {
dataFormToSend.addField(field);
}
}
return dataFormToSend;
}
return dataForm;
}
/**
* Returns true if the form is a form to fill out.
*
* @return if the form is a form to fill out.
*/
private boolean isFormType() {
return DataForm.Type.form == dataForm.getType();
}
/**
* Returns true if the form is a form to submit.
*
* @return if the form is a form to submit.
*/
private boolean isSubmitType() {
return DataForm.Type.submit == dataForm.getType();
}
/**
* Returns a new Form to submit the completed values. The new Form will include all the fields
* of the original form except for the fields of type FIXED. Only the HIDDEN fields will
* include the same value of the original form. The other fields of the new form MUST be
* completed. If a field remains with no answer when sending the completed form, then it won't
* be included as part of the completed form.<p>
*
* The reason why the fields with variables are included in the new form is to provide a model
* for binding with any UI. This means that the UIs will use the original form (of type
* "form") to learn how to render the form, but the UIs will bind the fields to the form of
* type submit.
*
* @return a Form to submit the completed values.
*/
public Form createAnswerForm() {
if (!isFormType()) {
throw new IllegalStateException("Only forms of type \"form\" could be answered");
}
// Create a new Form
Form form = new Form(DataForm.Type.submit);
for (FormField field : getFields()) {
// Add to the new form any type of field that includes a variable.
// Note: The fields of type FIXED are the only ones that don't specify a variable
if (field.getVariable() != null) {
FormField.Builder newField = FormField.builder(field.getVariable());
newField.setType(field.getType());
form.addField(newField.build());
// Set the answer ONLY to the hidden fields
if (field.getType() == FormField.Type.hidden) {
// Since a hidden field could have many values we need to collect them
// in a list
List<CharSequence> values = new ArrayList<>();
values.addAll(field.getValues());
form.setAnswer(field.getVariable(), values);
}
}
}
return form;
}
}

View file

@ -1,6 +1,6 @@
/** /**
* *
* Copyright 2003-2007 Jive Software, 2019 Florian Schmaus. * Copyright 2003-2007 Jive Software, 2019-2020 Florian Schmaus.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -33,6 +33,7 @@ import org.jivesoftware.smack.util.CollectionUtil;
import org.jivesoftware.smack.util.EqualsUtil; import org.jivesoftware.smack.util.EqualsUtil;
import org.jivesoftware.smack.util.HashCode; import org.jivesoftware.smack.util.HashCode;
import org.jivesoftware.smack.util.MultiMap; import org.jivesoftware.smack.util.MultiMap;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smack.util.XmlStringBuilder; import org.jivesoftware.smack.util.XmlStringBuilder;
import org.jivesoftware.smackx.xdata.packet.DataForm; import org.jivesoftware.smackx.xdata.packet.DataForm;
@ -43,10 +44,14 @@ import org.jxmpp.util.XmppDateTime;
* Represents a field of a form. The field could be used to represent a question to complete, * 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 * a completed question or a data returned from a search. The exact interpretation of the field
* depends on the context where the field is used. * depends on the context where the field is used.
* <p>
* Fields have a name, which is stored in the 'var' attribute of the field's XML representation.
* Field instances of all types, except of type "fixed" must have a name.
* </p>
* *
* @author Gaston Dombiak * @author Gaston Dombiak
*/ */
public final class FormField implements FullyQualifiedElement { public abstract class FormField implements FullyQualifiedElement {
public static final String ELEMENT = "field"; public static final String ELEMENT = "field";
@ -154,7 +159,7 @@ public final class FormField implements FullyQualifiedElement {
/** /**
* The field's name. Put as value in the 'var' attribute of &lt;field/&gt;. * The field's name. Put as value in the 'var' attribute of &lt;field/&gt;.
*/ */
private final String variable; private final String fieldName;
private final String label; private final String label;
@ -168,8 +173,6 @@ public final class FormField implements FullyQualifiedElement {
* The following four fields are cache values which are represented as child elements of </form> and hence also * The following four fields are cache values which are represented as child elements of </form> and hence also
* appear in formFieldChildElements. * appear in formFieldChildElements.
*/ */
private final List<Option> options;
private final List<CharSequence> values;
private final String description; private final String description;
private final boolean required; private final boolean required;
@ -181,22 +184,8 @@ public final class FormField implements FullyQualifiedElement {
return formFieldChildElementsMap.asUnmodifiableMultiMap(); return formFieldChildElementsMap.asUnmodifiableMultiMap();
} }
private FormField(String value) { protected FormField(Builder<?, ?> builder) {
variable = FORM_TYPE; fieldName = builder.fieldName;
type = Type.hidden;
label = null;
required = false;
description = null;
formFieldChildElements = Collections.singletonList(new Value(value));
values = Collections.singletonList(value);
options = Collections.emptyList();
formFieldChildElementsMap = createChildElementsMap();
}
private FormField(Builder builder) {
variable = builder.variable;
label = builder.label; label = builder.label;
type = builder.type; type = builder.type;
if (builder.formFieldChildElements != null) { if (builder.formFieldChildElements != null) {
@ -205,32 +194,21 @@ public final class FormField implements FullyQualifiedElement {
formFieldChildElements = Collections.emptyList(); formFieldChildElements = Collections.emptyList();
} }
if (variable == null && type != Type.fixed) { if (fieldName == null && type != Type.fixed) {
throw new IllegalArgumentException("The variable can only be null if the form is of type fixed"); throw new IllegalArgumentException("The variable can only be null if the form is of type fixed");
} }
String description = null; String description = null;
boolean requiredElementAsChild = false; boolean requiredElementAsChild = false;
ArrayList<Option> options = new ArrayList<>(formFieldChildElements.size());
ArrayList<CharSequence> values = new ArrayList<>(formFieldChildElements.size()); ArrayList<CharSequence> values = new ArrayList<>(formFieldChildElements.size());
for (FormFieldChildElement formFieldChildElement : formFieldChildElements) { for (FormFieldChildElement formFieldChildElement : formFieldChildElements) {
if (formFieldChildElement instanceof Value) { if (formFieldChildElement instanceof Description) {
Value value = (Value) formFieldChildElement;
values.add(value.getValue());
} else if (formFieldChildElement instanceof Option) {
Option option = (Option) formFieldChildElement;
options.add(option);
} else if (formFieldChildElement instanceof Description) {
description = ((Description) formFieldChildElement).getDescription(); description = ((Description) formFieldChildElement).getDescription();
// Required is a singleton instance. } else if (formFieldChildElement instanceof Required) {
} else if (formFieldChildElement == Required.INSTANCE) {
requiredElementAsChild = true; requiredElementAsChild = true;
} }
} }
options.trimToSize();
values.trimToSize(); values.trimToSize();
this.options = Collections.unmodifiableList(options);
this.values = Collections.unmodifiableList(values);
this.description = description; this.description = description;
required = requiredElementAsChild; required = requiredElementAsChild;
@ -262,16 +240,6 @@ public final class FormField implements FullyQualifiedElement {
return label; return label;
} }
/**
* Returns a List of the available options that the user has in order to answer
* the question.
*
* @return List of the available options.
*/
public List<Option> getOptions() {
return options;
}
/** /**
* Returns true if the question must be answered in order to complete the questionnaire. * Returns true if the question must be answered in order to complete the questionnaire.
* *
@ -301,10 +269,11 @@ public final class FormField implements FullyQualifiedElement {
* *
* @return a List of the default values or answered values of the question. * @return a List of the default values or answered values of the question.
*/ */
public List<CharSequence> getValues() { public abstract List<? extends CharSequence> getValues();
synchronized (values) {
return Collections.unmodifiableList(new ArrayList<>(values)); public boolean hasValueSet() {
} List<?> values = getValues();
return !values.isEmpty();
} }
/** /**
@ -316,7 +285,7 @@ public final class FormField implements FullyQualifiedElement {
* @since 4.3 * @since 4.3
*/ */
public List<String> getValuesAsString() { public List<String> getValuesAsString() {
List<CharSequence> valuesAsCharSequence = getValues(); List<? extends CharSequence> valuesAsCharSequence = getValues();
List<String> res = new ArrayList<>(valuesAsCharSequence.size()); List<String> res = new ArrayList<>(valuesAsCharSequence.size());
for (CharSequence value : valuesAsCharSequence) { for (CharSequence value : valuesAsCharSequence) {
res.add(value.toString()); res.add(value.toString());
@ -331,16 +300,12 @@ public final class FormField implements FullyQualifiedElement {
* @since 4.3 * @since 4.3
*/ */
public String getFirstValue() { public String getFirstValue() {
CharSequence firstValue; List<? extends CharSequence> values = getValues();
synchronized (values) {
if (values.isEmpty()) { if (values.isEmpty()) {
return null; return null;
} }
firstValue = values.get(0);
}
return firstValue.toString(); return values.get(0).toString();
} }
/** /**
@ -367,9 +332,26 @@ public final class FormField implements FullyQualifiedElement {
* </p> * </p>
* *
* @return the field's name. * @return the field's name.
* @deprecated use {@link #getFieldName()} instead.
*/ */
// TODO: Remove in Smack 4.5
@Deprecated
public String getVariable() { public String getVariable() {
return variable; return getFieldName();
}
/**
* Returns the field's name, also known as the variable name in case this is an filled out answer form.
* <p>
* According to XEP-4 § 3.2 the variable name (the 'var' attribute)
* "uniquely identifies the field in the context of the form" (if the field is not of type 'fixed', in which case
* the field "MAY possess a 'var' attribute")
* </p>
*
* @return the field's name.
*/
public String getFieldName() {
return fieldName;
} }
public FormFieldChildElement getFormFieldChildElement(QName qname) { public FormFieldChildElement getFormFieldChildElement(QName qname) {
@ -380,6 +362,10 @@ public final class FormField implements FullyQualifiedElement {
return formFieldChildElementsMap.getAll(qname); return formFieldChildElementsMap.getAll(qname);
} }
public List<FormFieldChildElement> getFormFieldChildElements() {
return formFieldChildElements;
}
@Override @Override
public String getElementName() { public String getElementName() {
return ELEMENT; return ELEMENT;
@ -395,28 +381,43 @@ public final class FormField implements FullyQualifiedElement {
return QNAME; return QNAME;
} }
public Builder buildAnswer() { protected transient List<FullyQualifiedElement> extraXmlChildElements;
return asBuilder().resetValues();
}
public Builder asBuilder() { protected void populateExtraXmlChildElements() {
return new Builder(this); List<? extends CharSequence> values = getValues();
extraXmlChildElements = new ArrayList<>(values.size());
for (CharSequence value : values) {
extraXmlChildElements.add(new Value(value));
}
} }
@Override @Override
public XmlStringBuilder toXML(XmlEnvironment enclosingNamespace) { public final XmlStringBuilder toXML(XmlEnvironment enclosingNamespace) {
return toXML(enclosingNamespace, true);
}
public final XmlStringBuilder toXML(XmlEnvironment enclosingNamespace, boolean includeType) {
XmlStringBuilder buf = new XmlStringBuilder(this, enclosingNamespace); XmlStringBuilder buf = new XmlStringBuilder(this, enclosingNamespace);
// Add attributes // Add attributes
buf.optAttribute("label", getLabel()); buf.optAttribute("label", getLabel());
buf.optAttribute("var", getVariable()); buf.optAttribute("var", getFieldName());
if (includeType) {
// If no 'type' is specified, the default is "text-single"; // If no 'type' is specified, the default is "text-single";
buf.attribute("type", getType(), Type.text_single); buf.attribute("type", getType(), Type.text_single);
}
if (formFieldChildElements.isEmpty()) { if (extraXmlChildElements == null) {
// If extraXmlChildElements is null, see if they should be populated.
populateExtraXmlChildElements();
}
if (formFieldChildElements.isEmpty() && extraXmlChildElements == null) {
buf.closeEmptyElement(); buf.closeEmptyElement();
} else { } else {
buf.rightAngleBracket(); buf.rightAngleBracket();
buf.optAppend(extraXmlChildElements);
buf.append(formFieldChildElements); buf.append(formFieldChildElements);
buf.closeElement(this); buf.closeElement(this);
@ -443,72 +444,115 @@ public final class FormField implements FullyQualifiedElement {
return toXML().toString().hashCode(); return toXML().toString().hashCode();
} }
public static FormField hiddenFormType(String value) { public static BooleanFormField.Builder booleanBuilder(String fieldName) {
return new FormField(value); return new BooleanFormField.Builder(fieldName);
} }
public static Builder builder(String fieldName) { public static TextSingleFormField.Builder fixedBuilder() {
return builder().setFieldName(fieldName); return fixedBuilder(null);
} }
public static Builder builder() { public static TextSingleFormField.Builder fixedBuilder(String fieldName) {
return new Builder(); return new TextSingleFormField.Builder(fieldName, Type.fixed);
} }
public static final class Builder { public static TextSingleFormField.Builder hiddenBuilder(String fieldName) {
private String variable; return new TextSingleFormField.Builder(fieldName, Type.hidden);
}
public static JidMultiFormField.Builder jidMultiBuilder(String fieldName) {
return new JidMultiFormField.Builder(fieldName);
}
public static JidSingleFormField.Builder jidSingleBuilder(String fieldName) {
return new JidSingleFormField.Builder(fieldName);
}
public static ListMultiFormField.Builder listMultiBuilder(String fieldName) {
return new ListMultiFormField.Builder(fieldName);
}
public static ListSingleFormField.Builder listSingleBuilder(String fieldName) {
return new ListSingleFormField.Builder(fieldName);
}
public static TextMultiFormField.Builder textMultiBuilder(String fieldName) {
return new TextMultiFormField.Builder(fieldName);
}
public static TextSingleFormField.Builder textPrivateBuilder(String fieldName) {
return new TextSingleFormField.Builder(fieldName, Type.text_private);
}
public static TextSingleFormField.Builder textSingleBuilder(String fieldName) {
return new TextSingleFormField.Builder(fieldName, Type.text_single);
}
public static TextSingleFormField.Builder builder(String fieldName) {
return textSingleBuilder(fieldName);
}
public static TextSingleFormField buildHiddenFormType(String formType) {
return hiddenBuilder(FORM_TYPE).setValue(formType).build();
}
public <F extends FormField> F ifPossibleAs(Class<F> formFieldClass) {
if (formFieldClass.isInstance(this)) {
return formFieldClass.cast(this);
}
return null;
}
public <F extends FormField> F ifPossibleAsOrThrow(Class<F> formFieldClass) {
F field = ifPossibleAs(formFieldClass);
if (field == null) {
throw new IllegalArgumentException();
}
return field;
}
public TextSingleFormField asHiddenFormTypeFieldIfPossible() {
TextSingleFormField textSingleFormField = ifPossibleAs(TextSingleFormField.class);
if (textSingleFormField == null) {
return null;
}
if (getType() != Type.hidden) {
return null;
}
if (!getFieldName().equals(FORM_TYPE)) {
return null;
}
return textSingleFormField;
}
public abstract static class Builder<F extends FormField, B extends Builder<?, ?>> {
private final String fieldName;
private final Type type;
private String label; private String label;
private Type type;
private List<FormFieldChildElement> formFieldChildElements; private List<FormFieldChildElement> formFieldChildElements;
private boolean disallowType; private boolean disallowType;
private boolean disallowFurtherFormFieldChildElements; private boolean disallowFurtherFormFieldChildElements;
private Builder() { protected Builder(String fieldName, Type type) {
if (StringUtils.isNullOrEmpty(fieldName) && type != Type.fixed) {
throw new IllegalArgumentException("Fields of type " + type + " must have a field name set");
}
this.fieldName = fieldName;
this.type = type;
} }
private Builder(FormField formField) { protected Builder(FormField formField) {
variable = formField.variable; // TODO: Is this still correct?
fieldName = formField.fieldName;
label = formField.label; label = formField.label;
type = formField.type; type = formField.type;
// Create a new modifiable list. // Create a new modifiable list.
formFieldChildElements = CollectionUtil.newListWith(formField.formFieldChildElements); formFieldChildElements = CollectionUtil.newListWith(formField.formFieldChildElements);
} }
public Builder setFieldName(String fieldName) {
return setVariable(fieldName);
}
public Builder setVariable(String variable) {
this.variable = variable;
return this;
}
/**
* Sets an indicative of the format for the data to answer.
*
* @param type an indicative of the format for the data to answer.
* @return a reference to this builder.
* @see Type
*/
public Builder setType(Type type) {
final Type oldType = this.type;
this.type = type;
try {
checkFormFieldChildElementsConsistency();
} catch (IllegalArgumentException e) {
this.type = oldType;
throw e;
}
return this;
}
/** /**
* Sets a description that provides extra clarification about the question. This information * 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 * could be presented to the user either in tool-tip, help button, or as a section of text
@ -520,10 +564,10 @@ public final class FormField implements FullyQualifiedElement {
* @param description provides extra clarification about the question. * @param description provides extra clarification about the question.
* @return a reference to this builder. * @return a reference to this builder.
*/ */
public Builder setDescription(String description) { public B setDescription(String description) {
Description descriptionElement = new Description(description); Description descriptionElement = new Description(description);
setOnlyElement(descriptionElement, Description.class); setOnlyElement(descriptionElement);
return this; return getThis();
} }
/** /**
@ -533,9 +577,18 @@ public final class FormField implements FullyQualifiedElement {
* @param label the label of the question. * @param label the label of the question.
* @return a reference to this builder. * @return a reference to this builder.
*/ */
public Builder setLabel(String label) { public B setLabel(String label) {
this.label = label; this.label = StringUtils.requireNotNullNorEmpty(label, "label must not be null or empty");
return this; return getThis();
}
/**
* Sets if the question must be answered in order to complete the questionnaire.
*
* @return a reference to this builder.
*/
public B setRequired() {
return setRequired(true);
} }
/** /**
@ -544,91 +597,63 @@ public final class FormField implements FullyQualifiedElement {
* @param required if the question must be answered in order to complete the questionnaire. * @param required if the question must be answered in order to complete the questionnaire.
* @return a reference to this builder. * @return a reference to this builder.
*/ */
public Builder setRequired(boolean required) { public B setRequired(boolean required) {
setOnlyElement(Required.INSTANCE, Required.class); if (required) {
return this; setOnlyElement(Required.INSTANCE);
}
return getThis();
} }
/** public B addFormFieldChildElements(Collection<? extends FormFieldChildElement> formFieldChildElements) {
* Adds a default value to the question if the question is part of a form to fill out. for (FormFieldChildElement formFieldChildElement : formFieldChildElements) {
* Otherwise, adds an answered value to the question. addFormFieldChildElement(formFieldChildElement);
* }
* @param value a default value or an answered value of the question. return getThis();
* @return a reference to this builder.
*/
public Builder addValue(CharSequence value) {
return addFormFieldChildElement(new Value(value));
} }
/** @SuppressWarnings("ModifyCollectionInEnhancedForLoop")
* Adds the given Date as XEP-0082 formated string by invoking {@link #addValue(CharSequence)} after the date public B addFormFieldChildElement(FormFieldChildElement newFormFieldChildElement) {
* instance was formated.
*
* @param date the date instance to add as XEP-0082 formated string.
* @return a reference to this builder.
*/
public Builder addValue(Date date) {
String dateString = XmppDateTime.formatXEP0082Date(date);
return addValue(dateString);
}
/**
* Adds a default values to the question if the question is part of a form to fill out.
* Otherwise, adds an answered values to the question.
*
* @param values default values or an answered values of the question.
* @return a reference to this builder.
*/
public Builder addValues(Collection<? extends CharSequence> values) {
for (CharSequence value : values) {
addValue(value);
}
return this;
}
public Builder addOption(String option) {
return addOption(new Option(option));
}
/**
* Adds an available options to the question that the user has in order to answer
* the question.
*
* @param option a new available option for the question.
* @return a reference to this builder.
*/
public Builder addOption(Option option) {
return addFormFieldChildElement(option);
}
public Builder addFormFieldChildElement(FormFieldChildElement formFieldChildElement) {
if (disallowFurtherFormFieldChildElements) { if (disallowFurtherFormFieldChildElements) {
throw new IllegalArgumentException(); throw new IllegalArgumentException();
} }
if (formFieldChildElement.requiresNoTypeSet() && type != null) { if (newFormFieldChildElement.requiresNoTypeSet() && type != null) {
throw new IllegalArgumentException("Elements of type " + formFieldChildElement.getClass() throw new IllegalArgumentException("Elements of type " + newFormFieldChildElement.getClass()
+ " can only be added to form fields where no type is set"); + " can only be added to form fields where no type is set");
} }
ensureThatFormFieldChildElementsIsSet(); ensureThatFormFieldChildElementsIsSet();
if (!formFieldChildElements.isEmpty() && formFieldChildElement.isExclusiveElement()) { if (!formFieldChildElements.isEmpty() && newFormFieldChildElement.isExclusiveElement()) {
throw new IllegalArgumentException("Elements of type " + formFieldChildElement.getClass() throw new IllegalArgumentException("Elements of type " + newFormFieldChildElement.getClass()
+ " must be the only child elements of a form field."); + " must be the only child elements of a form field.");
} }
disallowType = disallowType || formFieldChildElement.requiresNoTypeSet(); disallowType = disallowType || newFormFieldChildElement.requiresNoTypeSet();
disallowFurtherFormFieldChildElements = formFieldChildElement.isExclusiveElement(); disallowFurtherFormFieldChildElements = newFormFieldChildElement.isExclusiveElement();
formFieldChildElements.add(formFieldChildElement); formFieldChildElements.add(newFormFieldChildElement);
return this; for (FormFieldChildElement formFieldChildElement : formFieldChildElements) {
try {
formFieldChildElement.checkConsistency(this);
} catch (IllegalArgumentException e) {
// Remove the newly added form field child element if there it causes inconsistency.
formFieldChildElements.remove(newFormFieldChildElement);
throw e;
}
} }
public Builder resetValues() { return getThis();
}
protected abstract void resetInternal();
public B reset() {
resetInternal();
if (formFieldChildElements == null) { if (formFieldChildElements == null) {
return this; return getThis();
} }
// TODO: Use Java' stream API once Smack's minimum Android SDK level is 24 or higher. // TODO: Use Java' stream API once Smack's minimum Android SDK level is 24 or higher.
@ -642,12 +667,10 @@ public final class FormField implements FullyQualifiedElement {
disallowType = disallowFurtherFormFieldChildElements = false; disallowType = disallowFurtherFormFieldChildElements = false;
return this; return getThis();
} }
public FormField build() { public abstract F build();
return new FormField(this);
}
public Type getType() { public Type getType() {
return type; return type;
@ -659,7 +682,8 @@ public final class FormField implements FullyQualifiedElement {
} }
} }
private <E extends FormFieldChildElement> void setOnlyElement(E element, Class<E> elementClass) { private <E extends FormFieldChildElement> void setOnlyElement(E element) {
Class<?> elementClass = element.getClass();
ensureThatFormFieldChildElementsIsSet(); ensureThatFormFieldChildElementsIsSet();
for (int i = 0; i < formFieldChildElements.size(); i++) { for (int i = 0; i < formFieldChildElements.size(); i++) {
if (formFieldChildElements.get(i).getClass().equals(elementClass)) { if (formFieldChildElements.get(i).getClass().equals(elementClass)) {
@ -668,18 +692,10 @@ public final class FormField implements FullyQualifiedElement {
} }
} }
formFieldChildElements.add(0, element); addFormFieldChildElement(element);
} }
private void checkFormFieldChildElementsConsistency() { public abstract B getThis();
if (formFieldChildElements == null) {
return;
}
for (FormFieldChildElement formFiledChildElement : formFieldChildElements) {
formFiledChildElement.checkConsistency(this);
}
}
} }
/** /**
@ -689,11 +705,11 @@ public final class FormField implements FullyQualifiedElement {
} }
/** /**
* Represents the available option of a given FormField. * Represents the available options of a {@link ListSingleFormField} and {@link ListMultiFormField}.
* *
* @author Gaston Dombiak * @author Gaston Dombiak
*/ */
public static final class Option extends StandardFormFieldChildElement { public static final class Option implements FullyQualifiedElement {
public static final String ELEMENT = "option"; public static final String ELEMENT = "option";
@ -876,7 +892,7 @@ public final class FormField implements FullyQualifiedElement {
} }
} }
public static class Value extends StandardFormFieldChildElement { public static class Value implements FullyQualifiedElement {
public static final String ELEMENT = "value"; public static final String ELEMENT = "value";

View file

@ -1,6 +1,6 @@
/** /**
* *
* Copyright 2019 Florian Schmaus * Copyright 2019-2020 Florian Schmaus
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -32,8 +32,11 @@ public interface FormFieldChildElement extends FullyQualifiedElement {
return false; return false;
} }
default void checkConsistency(FormField.Builder formFieldBuilder) throws IllegalArgumentException { default void checkConsistency(FormField.Builder<?, ?> formFieldBuilder) throws IllegalArgumentException {
// Does nothing per default. // Does nothing per default.
} }
default void validate(FormField formField) throws IllegalArgumentException {
// Does nothing per default.
}
} }

View file

@ -0,0 +1,47 @@
/**
*
* Copyright 2020 Florian Schmaus
*
* 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.xdata;
import java.util.List;
public interface FormFieldWithOptions {
/**
* Returns a List of the available options that the user has in order to answer
* the question.
*
* @return List of the available options.
*/
List<FormField.Option> getOptions();
public interface Builder<B extends FormField.Builder<?, ?>> {
default B addOption(String option) {
return addOption(new FormField.Option(option));
}
/**
* Adds an available options to the question that the user has in order to answer
* the question.
*
* @param option a new available option for the question.
* @return a reference to this builder.
*/
B addOption(FormField.Option option);
}
}

View file

@ -0,0 +1,94 @@
/**
*
* Copyright 2020 Florian Schmaus.
*
* 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.xdata;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.jivesoftware.smack.util.CollectionUtil;
import org.jxmpp.jid.Jid;
public class JidMultiFormField extends FormField {
private final List<Jid> values;
protected JidMultiFormField(Builder builder) {
super(builder);
values = CollectionUtil.cloneAndSeal(builder.values);
}
@Override
public List<Jid> getValues() {
return values;
}
public Builder asBuilder() {
return new Builder(this);
}
public static final class Builder extends FormField.Builder<JidMultiFormField, JidMultiFormField.Builder> {
private List<Jid> values;
private Builder(JidMultiFormField jidMultiFormField) {
super(jidMultiFormField);
values = CollectionUtil.newListWith(jidMultiFormField.getValues());
}
Builder(String fieldName) {
super(fieldName, FormField.Type.jid_multi);
}
private void ensureValuesAreInitialized() {
if (values == null) {
values = new ArrayList<>();
}
}
@Override
protected void resetInternal() {
values = null;
}
public Builder addValue(Jid jid) {
ensureValuesAreInitialized();
values.add(jid);
return this;
}
public Builder addValues(Collection<? extends Jid> jids) {
ensureValuesAreInitialized();
values.addAll(jids);
return this;
}
@Override
public JidMultiFormField build() {
return new JidMultiFormField(this);
}
@Override
public Builder getThis() {
return this;
}
}
}

View file

@ -0,0 +1,71 @@
/**
*
* Copyright 2020 Florian Schmaus.
*
* 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.xdata;
import org.jxmpp.jid.Jid;
public class JidSingleFormField extends SingleValueFormField {
private final Jid value;
protected JidSingleFormField(Builder builder) {
super(builder);
value = builder.value;
}
@Override
public Jid getValue() {
return value;
}
public Builder asBuilder() {
return new Builder(this);
}
public static final class Builder extends FormField.Builder<JidSingleFormField, JidSingleFormField.Builder> {
private Jid value;
private Builder(JidSingleFormField jidSingleFormField) {
super(jidSingleFormField);
value = jidSingleFormField.getValue();
}
Builder(String fieldName) {
super(fieldName, FormField.Type.jid_single);
}
@Override
protected void resetInternal() {
value = null;
}
public Builder setValue(Jid value) {
this.value = value;
return this;
}
@Override
public JidSingleFormField build() {
return new JidSingleFormField(this);
}
@Override
public Builder getThis() {
return this;
}
}
}

View file

@ -0,0 +1,88 @@
/**
*
* Copyright 2020 Florian Schmaus
*
* 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.xdata;
import java.util.ArrayList;
import java.util.List;
import org.jivesoftware.smack.util.CollectionUtil;
public class ListMultiFormField extends AbstractMultiFormField implements FormFieldWithOptions {
private final List<Option> options;
protected ListMultiFormField(Builder builder) {
super(builder);
options = CollectionUtil.cloneAndSeal(builder.options);
}
@Override
public List<Option> getOptions() {
return options;
}
@Override
protected void populateExtraXmlChildElements() {
super.populateExtraXmlChildElements();
extraXmlChildElements.addAll(options);
}
public Builder asBuilder() {
return new Builder(this);
}
public static final class Builder
extends AbstractMultiFormField.Builder<ListMultiFormField, ListMultiFormField.Builder>
implements FormFieldWithOptions.Builder<Builder> {
private List<Option> options;
private Builder(ListMultiFormField textMultiFormField) {
super(textMultiFormField);
}
Builder(String fieldName) {
super(fieldName, FormField.Type.list_multi);
}
@Override
public Builder addValue(CharSequence value) {
return super.addValueVerbatim(value);
}
@Override
public Builder addOption(Option option) {
if (options == null) {
options = new ArrayList<>();
}
options.add(option);
return this;
}
@Override
public ListMultiFormField build() {
return new ListMultiFormField(this);
}
@Override
public Builder getThis() {
return this;
}
}
}

View file

@ -0,0 +1,87 @@
/**
*
* Copyright 2020 Florian Schmaus
*
* 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.xdata;
import java.util.ArrayList;
import java.util.List;
import org.jivesoftware.smack.util.CollectionUtil;
public class ListSingleFormField extends AbstractSingleStringValueFormField implements FormFieldWithOptions {
private final List<Option> options;
protected ListSingleFormField(Builder builder) {
super(builder);
options = CollectionUtil.cloneAndSeal(builder.options);
}
@Override
public List<Option> getOptions() {
return options;
}
public Builder asBuilder() {
return new Builder(this);
}
@Override
protected void populateExtraXmlChildElements() {
extraXmlChildElements = new ArrayList<>(1 + options.size());
CharSequence value = getValue();
if (value != null) {
extraXmlChildElements.add(new FormField.Value(value));
}
extraXmlChildElements.addAll(options);
}
public static final class Builder
extends AbstractSingleStringValueFormField.Builder<ListSingleFormField, ListSingleFormField.Builder>
implements FormFieldWithOptions.Builder<Builder> {
private List<Option> options;
private Builder(ListSingleFormField textSingleFormField) {
super(textSingleFormField);
}
Builder(String fieldName) {
super(fieldName, FormField.Type.list_single);
}
@Override
public Builder addOption(Option option) {
if (options == null) {
options = new ArrayList<>();
}
options.add(option);
return this;
}
@Override
public ListSingleFormField build() {
return new ListSingleFormField(this);
}
@Override
public Builder getThis() {
return this;
}
}
}

View file

@ -0,0 +1,48 @@
/**
*
* Copyright 2020 Florian Schmaus.
*
* 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.xdata;
import java.util.Collections;
import java.util.List;
public abstract class SingleValueFormField extends FormField {
protected SingleValueFormField(Builder<?, ?> builder) {
super(builder);
}
@Override
public final List<CharSequence> getValues() {
CharSequence value = getValue();
if (value == null) {
return Collections.emptyList();
}
return Collections.singletonList(value);
}
public abstract CharSequence getValue();
@Override
protected void populateExtraXmlChildElements() {
CharSequence value = getValue();
if (value == null) {
return;
}
extraXmlChildElements = Collections.singletonList(new Value(value));
}
}

View file

@ -0,0 +1,73 @@
/**
*
* Copyright 2020 Florian Schmaus.
*
* 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.xdata;
import java.util.List;
import org.jivesoftware.smack.util.StringUtils;
public class TextMultiFormField extends AbstractMultiFormField {
protected TextMultiFormField(Builder builder) {
super(builder);
}
public void addValuesWithNewlines(StringBuilder sb) {
for (CharSequence value : getValues()) {
sb.append(value);
}
}
public StringBuilder getValueswithNewlines() {
StringBuilder sb = new StringBuilder();
addValuesWithNewlines(sb);
return sb;
}
public Builder asBuilder() {
return new Builder(this);
}
public static final class Builder extends AbstractMultiFormField.Builder<TextMultiFormField, TextMultiFormField.Builder> {
private Builder(TextMultiFormField textMultiFormField) {
super(textMultiFormField);
}
Builder(String fieldName) {
super(fieldName, FormField.Type.text_multi);
}
@Override
public Builder addValue(CharSequence valueCharSequence) {
String value = valueCharSequence.toString();
List<String> lines = StringUtils.splitLinesPortable(value);
return addValues(lines);
}
@Override
public TextMultiFormField build() {
return new TextMultiFormField(this);
}
@Override
public Builder getThis() {
return this;
}
}
}

View file

@ -0,0 +1,51 @@
/**
*
* Copyright 2020 Florian Schmaus.
*
* 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.xdata;
public class TextSingleFormField extends AbstractSingleStringValueFormField {
protected TextSingleFormField(Builder builder) {
super(builder);
}
public Builder asBuilder() {
return new Builder(this);
}
public static final class Builder
extends AbstractSingleStringValueFormField.Builder<TextSingleFormField, TextSingleFormField.Builder> {
private Builder(TextSingleFormField textSingleFormField) {
super(textSingleFormField);
}
Builder(String fieldName, FormField.Type type) {
super(fieldName, type);
}
@Override
public TextSingleFormField build() {
return new TextSingleFormField(this);
}
@Override
public Builder getThis() {
return this;
}
}
}

View file

@ -1,6 +1,6 @@
/** /**
* *
* Copyright 2014-2019 Florian Schmaus * Copyright 2014-2020 Florian Schmaus
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -31,9 +31,6 @@ import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
import org.jivesoftware.smackx.xdata.packet.DataForm; import org.jivesoftware.smackx.xdata.packet.DataForm;
import org.jivesoftware.smackx.xdata.provider.DescriptionProvider; import org.jivesoftware.smackx.xdata.provider.DescriptionProvider;
import org.jivesoftware.smackx.xdata.provider.FormFieldChildElementProviderManager; import org.jivesoftware.smackx.xdata.provider.FormFieldChildElementProviderManager;
import org.jivesoftware.smackx.xdata.provider.OptionProvider;
import org.jivesoftware.smackx.xdata.provider.RequiredProvider;
import org.jivesoftware.smackx.xdata.provider.ValueProvider;
import org.jxmpp.jid.Jid; import org.jxmpp.jid.Jid;
@ -46,10 +43,7 @@ public final class XDataManager extends Manager {
static { static {
FormFieldChildElementProviderManager.addFormFieldChildElementProvider( FormFieldChildElementProviderManager.addFormFieldChildElementProvider(
new DescriptionProvider(), new DescriptionProvider()
new OptionProvider(),
new RequiredProvider(),
new ValueProvider()
); );
XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() { XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() {

View file

@ -0,0 +1,267 @@
/**
*
* Copyright 2020 Florian Schmaus
*
* 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.xdata.form;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jivesoftware.smackx.xdata.AbstractMultiFormField;
import org.jivesoftware.smackx.xdata.AbstractSingleStringValueFormField;
import org.jivesoftware.smackx.xdata.FormField;
import org.jivesoftware.smackx.xdata.FormField.Type;
import org.jivesoftware.smackx.xdata.FormFieldChildElement;
import org.jivesoftware.smackx.xdata.packet.DataForm;
import org.jxmpp.jid.Jid;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.jid.util.JidUtil;
import org.jxmpp.stringprep.XmppStringprepException;
import org.jxmpp.util.XmppDateTime;
public class FillableForm extends FilledForm implements FormWriter {
private final Set<String> requiredFields;
private final Set<String> filledRequiredFields = new HashSet<>();
private final Set<String> missingRequiredFields = new HashSet<>();
private final Map<String, FormField> filledFields = new HashMap<>();
public FillableForm(DataForm dataForm) {
super(dataForm);
if (dataForm.getType() != DataForm.Type.form) {
throw new IllegalArgumentException();
}
Set<String> requiredFields = new HashSet<>();
for (FormField formField : dataForm.getFields()) {
if (formField.isRequired()) {
String fieldName = formField.getFieldName();
requiredFields.add(fieldName);
missingRequiredFields.add(fieldName);
}
}
this.requiredFields = Collections.unmodifiableSet(requiredFields);
}
protected void writeListMulti(String fieldName, List<? extends CharSequence> values) {
FormField formField = FormField.listMultiBuilder(fieldName)
.addValues(values)
.build();
write(formField);
}
protected void writeTextSingle(String fieldName, CharSequence value) {
FormField formField = FormField.textSingleBuilder(fieldName)
.setValue(value)
.build();
write(formField);
}
protected void writeBoolean(String fieldName, boolean value) {
FormField formField = FormField.booleanBuilder(fieldName)
.setValue(value)
.build();
write(formField);
}
protected void write(String fieldName, int value) {
writeTextSingle(fieldName, Integer.toString(value));
}
protected void write(String fieldName, Date date) {
writeTextSingle(fieldName, XmppDateTime.formatXEP0082Date(date));
}
public void setAnswer(String fieldName, Collection<? extends CharSequence> answers) {
FormField blankField = getFieldOrThrow(fieldName);
FormField.Type type = blankField.getType();
FormField filledFormField;
switch (type) {
case list_multi:
case text_multi:
filledFormField = createMultiKindFieldbuilder(fieldName, type)
.addValues(answers)
.build();
break;
case jid_multi:
List<Jid> jids = new ArrayList<>(answers.size());
List<XmppStringprepException> exceptions = new ArrayList<>();
JidUtil.jidsFrom(answers, jids, exceptions);
if (!exceptions.isEmpty()) {
// TODO: Report all exceptions here.
throw new IllegalArgumentException(exceptions.get(0));
}
filledFormField = FormField.jidMultiBuilder(fieldName)
.addValues(jids)
.build();
break;
default:
throw new IllegalArgumentException("");
}
write(filledFormField);
}
private static AbstractMultiFormField.Builder<?, ?> createMultiKindFieldbuilder(String fieldName, FormField.Type type) {
switch (type) {
case list_multi:
return FormField.listMultiBuilder(fieldName);
case text_multi:
return FormField.textMultiBuilder(fieldName);
default:
throw new IllegalArgumentException();
}
}
public void setAnswer(String fieldName, int answer) {
setAnswer(fieldName, Integer.toString(answer));
}
public void setAnswer(String fieldName, CharSequence answer) {
FormField blankField = getFieldOrThrow(fieldName);
FormField.Type type = blankField.getType();
FormField filledFormField;
switch (type) {
case list_multi:
case jid_multi:
throw new IllegalArgumentException("Can not answer fields of type '" + type + "' with a CharSequence");
case fixed:
throw new IllegalArgumentException("Fields of type 'fixed' are not answerable");
case list_single:
case text_private:
case text_single:
case hidden:
filledFormField = createSingleKindFieldBuilder(fieldName, type)
.setValue(answer)
.build();
break;
case bool:
filledFormField = FormField.booleanBuilder(fieldName)
.setValue(answer)
.build();
break;
case jid_single:
Jid jid;
try {
jid = JidCreate.from(answer);
} catch (XmppStringprepException e) {
throw new IllegalArgumentException(e);
}
filledFormField = FormField.jidSingleBuilder(fieldName)
.setValue(jid)
.build();
break;
case text_multi:
filledFormField = createMultiKindFieldbuilder(fieldName, type)
.addValue(answer)
.build();
break;
default:
throw new AssertionError();
}
write(filledFormField);
}
private static AbstractSingleStringValueFormField.Builder<?, ?> createSingleKindFieldBuilder(String fieldName, FormField.Type type) {
switch (type) {
case text_private:
return FormField.textPrivateBuilder(fieldName);
case text_single:
return FormField.textSingleBuilder(fieldName);
case hidden:
return FormField.hiddenBuilder(fieldName);
case list_multi:
return FormField.listSingleBuilder(fieldName);
default:
throw new IllegalArgumentException();
}
}
public void setAnswer(String fieldName, boolean answer) {
FormField blankField = getFieldOrThrow(fieldName);
if (blankField.getType() != Type.bool) {
throw new IllegalArgumentException();
}
FormField filledFormField = FormField.booleanBuilder(fieldName)
.setValue(answer)
.build();
write(filledFormField);
}
@Override
public final void write(FormField filledFormField) {
if (filledFormField.getType() == FormField.Type.fixed) {
throw new IllegalArgumentException();
}
if (!filledFormField.hasValueSet()) {
throw new IllegalArgumentException();
}
String fieldName = filledFormField.getFieldName();
if (!getDataForm().hasField(fieldName)) {
throw new IllegalArgumentException();
}
if (filledFields.containsKey(fieldName)) {
throw new IllegalArgumentException();
}
// Perform validation, e.g. using XEP-0122.
// TODO: We could also perform list-* option validation, but this has to take xep122's <open/> into account.
FormField formFieldPrototype = getDataForm().getField(fieldName);
for (FormFieldChildElement formFieldChildelement : formFieldPrototype.getFormFieldChildElements()) {
formFieldChildelement.validate(filledFormField);
}
filledFields.put(fieldName, filledFormField);
if (requiredFields.contains(fieldName)) {
filledRequiredFields.add(fieldName);
missingRequiredFields.remove(fieldName);
}
}
@Override
public FormField read(String fieldName) {
FormField filledField = filledFields.get(fieldName);
if (filledField != null) {
return filledField;
}
return super.read(fieldName);
}
public DataForm getDataFormToSubmit() {
if (!missingRequiredFields.isEmpty()) {
throw new IllegalStateException("Not all required fields filled. Missing: " + missingRequiredFields);
}
DataForm dataFormToSend = DataForm.builder()
.addField(formTypeFormField)
.addFields(filledFields.values())
.build();
return dataFormToSend;
}
}

View file

@ -0,0 +1,97 @@
/**
*
* Copyright 2020 Florian Schmaus
*
* 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.xdata.form;
import org.jivesoftware.smack.util.Objects;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smackx.xdata.FormField;
import org.jivesoftware.smackx.xdata.TextSingleFormField;
import org.jivesoftware.smackx.xdata.packet.DataForm;
import org.jivesoftware.smackx.xdata.packet.DataForm.Type;
public abstract class FilledForm implements FormReader {
private final DataForm dataForm;
protected final TextSingleFormField formTypeFormField;
public FilledForm(DataForm dataForm) {
this.dataForm = Objects.requireNonNull(dataForm);
String formType = dataForm.getFormType();
if (StringUtils.isNullOrEmpty(formType)) {
throw new IllegalArgumentException("The provided data form has no hidden FROM_TYPE field.");
}
if (dataForm.getType() == Type.cancel) {
throw new IllegalArgumentException("Forms of type 'cancel' are not filled nor fillable");
}
formTypeFormField = dataForm.getHiddenFormTypeField();
}
@Override
public FormField read(String fieldName) {
return dataForm.getField(fieldName);
}
public String getTitle() {
return dataForm.getTitle();
}
public StringBuilder getInstructions() {
StringBuilder sb = new StringBuilder();
for (String instruction : dataForm.getInstructions()) {
sb.append(instruction).append('\n');
}
return sb;
}
public DataForm getDataForm() {
return dataForm;
}
public String getFormType() {
if (formTypeFormField == null) {
return null;
}
return formTypeFormField.getValue();
}
public boolean hasField(String fieldName) {
return dataForm.hasField(fieldName);
}
public FormField getField(String fieldName) {
return dataForm.getField(fieldName);
}
protected FormField getFieldOrThrow(String fieldName) {
FormField formField = getField(fieldName);
if (formField == null) {
throw new IllegalArgumentException("No field named " + fieldName);
}
return formField;
}
protected static void ensureFormType(DataForm dataForm, String formType) {
String dataFormType = dataForm.getFormType();
if (!formType.equals(dataFormType)) {
throw new IllegalArgumentException("The provided data form must be of type '" + formType
+ "', this one was of type '" + dataFormType + '\'');
}
}
}

View file

@ -0,0 +1,44 @@
/**
*
* Copyright 2020 Florian Schmaus
*
* 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.xdata.form;
import org.jivesoftware.smack.packet.StanzaView;
import org.jivesoftware.smackx.xdata.packet.DataForm;
import org.jivesoftware.smackx.xdata.packet.DataForm.Type;
public class Form extends FilledForm {
public Form(DataForm dataForm) {
super(dataForm);
if (dataForm.getType() != Type.form) {
throw new IllegalArgumentException();
}
}
public FillableForm getFillableForm() {
return new FillableForm(getDataForm());
}
public static Form from(StanzaView stanzaView) {
DataForm dataForm = DataForm.from(stanzaView);
if (dataForm == null || dataForm.getType() != Type.form) {
return null;
}
return new Form(dataForm);
}
}

View file

@ -0,0 +1,90 @@
/**
*
* Copyright 2020 Florian Schmaus
*
* 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.xdata.form;
import java.text.ParseException;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import org.jivesoftware.smackx.xdata.AbstractMultiFormField;
import org.jivesoftware.smackx.xdata.AbstractSingleStringValueFormField;
import org.jivesoftware.smackx.xdata.BooleanFormField;
import org.jivesoftware.smackx.xdata.FormField;
import org.jxmpp.util.XmppDateTime;
public interface FormReader {
FormField read(String fieldName);
default String readFirstValue(String fieldName) {
FormField formField = read(fieldName);
if (formField == null) {
return null;
}
return formField.getFirstValue();
}
default List<? extends CharSequence> readValues(String fieldName) {
FormField formField = read(fieldName);
if (formField == null) {
return Collections.emptyList();
}
return formField.getValues();
}
default List<String> readStringValues(String fieldName) {
FormField formField = read(fieldName);
if (formField == null) {
return Collections.emptyList();
}
AbstractMultiFormField multiFormField = formField.ifPossibleAs(AbstractMultiFormField.class);
return multiFormField.getValues();
}
default Boolean readBoolean(String fieldName) {
FormField formField = read(fieldName);
if (formField == null) {
return null;
}
BooleanFormField booleanFormField = formField.ifPossibleAs(BooleanFormField.class);
return booleanFormField.getValueAsBoolean();
}
default Integer readInteger(String fieldName) {
FormField formField = read(fieldName);
if (formField == null) {
return null;
}
AbstractSingleStringValueFormField textSingleFormField = formField.ifPossibleAs(AbstractSingleStringValueFormField.class);
return textSingleFormField.getValueAsInt();
}
default Date readDate(String fieldName) throws ParseException {
FormField formField = read(fieldName);
if (formField == null) {
return null;
}
AbstractSingleStringValueFormField textSingleFormField = formField.ifPossibleAs(AbstractSingleStringValueFormField.class);
String value = textSingleFormField.getValue();
if (value == null) {
return null;
}
return XmppDateTime.parseDate(value);
}
}

View file

@ -0,0 +1,25 @@
/**
*
* Copyright 2020 Florian Schmaus
*
* 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.xdata.form;
import org.jivesoftware.smackx.xdata.FormField;
public interface FormWriter {
void write(FormField field);
}

View file

@ -0,0 +1,21 @@
/**
*
* Copyright 2020 Florian Schmaus
*
* 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.
*/
/**
* Smacks high-level API for XEP-0004: Data Forms.
*/
package org.jivesoftware.smackx.xdata.form;

View file

@ -1,6 +1,6 @@
/** /**
* *
* Copyright 2003-2007 Jive Software. * Copyright 2003-2007 Jive Software, 2020 Florian Schmaus.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -20,8 +20,8 @@ package org.jivesoftware.smackx.xdata.packet;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
@ -32,17 +32,26 @@ import org.jivesoftware.smack.packet.Element;
import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.packet.StanzaView; import org.jivesoftware.smack.packet.StanzaView;
import org.jivesoftware.smack.packet.XmlEnvironment; import org.jivesoftware.smack.packet.XmlEnvironment;
import org.jivesoftware.smack.util.CollectionUtil;
import org.jivesoftware.smack.util.Objects;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smack.util.XmlStringBuilder; import org.jivesoftware.smack.util.XmlStringBuilder;
import org.jivesoftware.smackx.formtypes.FormFieldRegistry;
import org.jivesoftware.smackx.xdata.FormField; import org.jivesoftware.smackx.xdata.FormField;
import org.jivesoftware.smackx.xdata.TextSingleFormField;
/** /**
* Represents a form that could be use for gathering data as well as for reporting data * Represents a form that could be use for gathering data as well as for reporting data
* returned from a search. * returned from a search.
* <p>
* Note that unlike many other things in XMPP, the order of the form fields is actually
* Important in data forms.
* </p>
* *
* @author Gaston Dombiak * @author Gaston Dombiak
*/ */
public class DataForm implements ExtensionElement { public final class DataForm implements ExtensionElement {
public static final String NAMESPACE = "jabber:x:data"; public static final String NAMESPACE = "jabber:x:data";
public static final String ELEMENT = "x"; public static final String ELEMENT = "x";
@ -76,16 +85,29 @@ public class DataForm implements ExtensionElement {
} }
} }
private Type type; private final Type type;
private String title; private final String title;
private final List<String> instructions = new ArrayList<>(); private final List<String> instructions;
private ReportedData reportedData; private final ReportedData reportedData;
private final List<Item> items = new ArrayList<>(); private final List<Item> items;
private final Map<String, FormField> fields = new LinkedHashMap<>(); private final List<FormField> fields;
private final List<Element> extensionElements = new ArrayList<>(); private final Map<String, FormField> fieldsMap;
private final List<Element> extensionElements;
public DataForm(Type type) { private DataForm(Builder builder) {
this.type = type; type = builder.type;
title = builder.title;
instructions = CollectionUtil.cloneAndSeal(builder.instructions);
reportedData = builder.reportedData;
items = CollectionUtil.cloneAndSeal(builder.items);
fields = CollectionUtil.cloneAndSeal(builder.fields);
fieldsMap = CollectionUtil.cloneAndSeal(builder.fieldsMap);
extensionElements = CollectionUtil.cloneAndSeal(builder.extensionElements);
// Ensure that the types of the form fields of every data form is known by registering such fields.
if (type == Type.form) {
FormFieldRegistry.register(this);
}
} }
/** /**
@ -117,9 +139,7 @@ public class DataForm implements ExtensionElement {
* @return a List of the list of instructions that explain how to fill out the form. * @return a List of the list of instructions that explain how to fill out the form.
*/ */
public List<String> getInstructions() { public List<String> getInstructions() {
synchronized (instructions) { return instructions;
return Collections.unmodifiableList(new ArrayList<>(instructions));
}
} }
/** /**
@ -137,9 +157,7 @@ public class DataForm implements ExtensionElement {
* @return a List of the items returned from a search. * @return a List of the items returned from a search.
*/ */
public List<Item> getItems() { public List<Item> getItems() {
synchronized (items) { return items;
return Collections.unmodifiableList(new ArrayList<>(items));
}
} }
/** /**
@ -148,46 +166,29 @@ public class DataForm implements ExtensionElement {
* @return a List of the fields that are part of the form. * @return a List of the fields that are part of the form.
*/ */
public List<FormField> getFields() { public List<FormField> getFields() {
synchronized (fields) { return fields;
return new ArrayList<>(fields.values());
}
} }
/** /**
* Return the form field with the given variable name or null. * Return the form field with the given variable name or null.
* *
* @param variableName TODO javadoc me please * @param fieldName the name of the field (the value of the 'var' (variable) attribute)
* @return the form field or null. * @return the form field or null.
* @since 4.1 * @since 4.1
*/ */
public FormField getField(String variableName) { public FormField getField(String fieldName) {
synchronized (fields) { return fieldsMap.get(fieldName);
return fields.get(variableName);
}
}
public FormField replaceField(FormField field) {
String fieldVariableName = field.getVariable();
synchronized (fields) {
if (!fields.containsKey(fieldVariableName)) {
throw new IllegalArgumentException("No field with the name " + fieldVariableName + " exists");
}
return fields.put(fieldVariableName, field);
}
} }
/** /**
* Check if a form field with the given variable name exists. * Check if a form field with the given variable name exists.
* *
* @param variableName TODO javadoc me please * @param fieldName the name of the field.
* @return true if a form field with the variable name exists, false otherwise. * @return true if a form field with the variable name exists, false otherwise.
* @since 4.2 * @since 4.2
*/ */
public boolean hasField(String variableName) { public boolean hasField(String fieldName) {
synchronized (fields) { return fieldsMap.containsKey(fieldName);
return fields.containsKey(variableName);
}
} }
@Override @Override
@ -200,108 +201,8 @@ public class DataForm implements ExtensionElement {
return NAMESPACE; return NAMESPACE;
} }
/**
* Sets the description of the data. It is similar to the title on a web page or an X window.
* You can put a &lt;title/&gt; on either a form to fill out, or a set of data results.
*
* @param title description of the data.
*/
public void setTitle(String title) {
this.title = title;
}
/**
* Sets the list of instructions that explain how to fill out the form and what the form is
* about. The dataform could include multiple instructions since each instruction could not
* contain newlines characters.
*
* @param instructions list of instructions that explain how to fill out the form.
*/
public void setInstructions(List<String> instructions) {
synchronized (this.instructions) {
this.instructions.clear();
this.instructions.addAll(instructions);
}
}
/**
* Sets the fields that will be returned from a search.
*
* @param reportedData the fields that will be returned from a search.
*/
public void setReportedData(ReportedData reportedData) {
this.reportedData = reportedData;
}
/**
* Adds a new field as part of the form.
*
* @param field the field to add to the form.
*/
public void addField(FormField field) {
String fieldVariableName = field.getVariable();
// Form field values must be unique unless they are of type 'fixed', in
// which case their variable name may be 'null', and therefore could
// appear multiple times within the same form.
if (fieldVariableName != null && hasField(fieldVariableName)) {
throw new IllegalArgumentException("This data form already contains a form field with the variable name '"
+ fieldVariableName + "'");
}
synchronized (fields) {
fields.put(fieldVariableName, field);
}
}
/**
* Add the given fields to this form.
*
* @param fieldsToAdd TODO javadoc me please
* @return true if a field was overridden.
* @since 4.3.0
*/
public boolean addFields(Collection<FormField> fieldsToAdd) {
boolean fieldOverridden = false;
synchronized (fields) {
for (FormField field : fieldsToAdd) {
FormField previousField = fields.put(field.getVariable(), field);
if (previousField != null) {
fieldOverridden = true;
}
}
}
return fieldOverridden;
}
/**
* Adds a new instruction to the list of instructions that explain how to fill out the form
* and what the form is about. The dataform could include multiple instructions since each
* instruction could not contain newlines characters.
*
* @param instruction the new instruction that explain how to fill out the form.
*/
public void addInstruction(String instruction) {
synchronized (instructions) {
instructions.add(instruction);
}
}
/**
* Adds a new item returned from a search.
*
* @param item the item returned from a search.
*/
public void addItem(Item item) {
synchronized (items) {
items.add(item);
}
}
public void addExtensionElement(Element element) {
extensionElements.add(element);
}
public List<Element> getExtensionElements() { public List<Element> getExtensionElements() {
return Collections.unmodifiableList(extensionElements); return extensionElements;
} }
/** /**
@ -324,13 +225,13 @@ public class DataForm implements ExtensionElement {
* @return the hidden FORM_TYPE field or null. * @return the hidden FORM_TYPE field or null.
* @since 4.1 * @since 4.1
*/ */
public FormField getHiddenFormTypeField() { public TextSingleFormField getHiddenFormTypeField() {
FormField field = getField(FormField.FORM_TYPE); FormField field = getField(FormField.FORM_TYPE);
if (field != null && field.getType() == FormField.Type.hidden) { if (field == null) {
return field;
}
return null; return null;
} }
return field.asHiddenFormTypeFieldIfPossible();
}
/** /**
* Returns true if this DataForm has at least one FORM_TYPE field which is * Returns true if this DataForm has at least one FORM_TYPE field which is
@ -348,27 +249,33 @@ public class DataForm implements ExtensionElement {
buf.attribute("type", getType()); buf.attribute("type", getType());
buf.rightAngleBracket(); buf.rightAngleBracket();
xmlEnvironment = buf.getXmlEnvironment();
buf.optElement("title", getTitle()); buf.optElement("title", getTitle());
for (String instruction : getInstructions()) { for (String instruction : getInstructions()) {
buf.element("instructions", instruction); buf.element("instructions", instruction);
} }
// Append the list of fields returned from a search // Append the list of fields returned from a search
if (getReportedData() != null) { buf.optElement(getReportedData());
buf.append(getReportedData().toXML());
}
// Loop through all the items returned from a search and append them to the string buffer // Loop through all the items returned from a search and append them to the string buffer
for (Item item : getItems()) { buf.append(getItems());
buf.append(item.toXML());
}
// Add all form fields. // Add all form fields.
buf.append(getFields()); // We do not need to include the type for data forms of the type submit.
for (Element element : extensionElements) { boolean includeType = getType() != Type.submit;
buf.append(element.toXML()); for (FormField formField : getFields()) {
buf.append(formField.toXML(xmlEnvironment, includeType));
} }
buf.append(getExtensionElements());
buf.closeElement(this); buf.closeElement(this);
return buf; return buf;
} }
public Builder asBuilder() {
return new Builder(this);
}
/** /**
* Get data form from a stanza. * Get data form from a stanza.
* *
@ -376,7 +283,7 @@ public class DataForm implements ExtensionElement {
* @return the DataForm or null * @return the DataForm or null
*/ */
public static DataForm from(StanzaView stanzaView) { public static DataForm from(StanzaView stanzaView) {
return stanzaView.getExtension(DataForm.class); return from(stanzaView, null);
} }
/** /**
@ -388,6 +295,9 @@ public class DataForm implements ExtensionElement {
* @since 4.4.0 * @since 4.4.0
*/ */
public static DataForm from(StanzaView stanzaView, String formType) { public static DataForm from(StanzaView stanzaView, String formType) {
if (formType == null) {
return stanzaView.getExtension(DataForm.class);
}
List<DataForm> dataForms = stanzaView.getExtensions(DataForm.class); List<DataForm> dataForms = stanzaView.getExtensions(DataForm.class);
return from(dataForms, formType); return from(dataForms, formType);
} }
@ -429,6 +339,189 @@ public class DataForm implements ExtensionElement {
return null; return null;
} }
/**
* Get a new data form builder with the form type set to {@link Type#submit}.
*
* @return a new data form builder.
*/
public static Builder builder() {
return new Builder();
}
public static Builder builder(Type type) {
return new Builder(type);
}
public static final class Builder {
private Type type;
private String title;
private List<String> instructions;
private ReportedData reportedData;
private List<Item> items;
private List<FormField> fields = new ArrayList<>();
private Map<String, FormField> fieldsMap = new HashMap<>();
private List<Element> extensionElements;
private Builder() {
this(Type.submit);
}
private Builder(Type type) {
this.type = type;
}
private Builder(DataForm dataForm) {
type = dataForm.getType();
title = dataForm.getTitle();
instructions = dataForm.getInstructions();
reportedData = dataForm.getReportedData();
items = CollectionUtil.newListWith(dataForm.getItems());
fields = CollectionUtil.newListWith(dataForm.getFields());
fieldsMap = new HashMap<>(dataForm.fieldsMap);
extensionElements = CollectionUtil.newListWith(dataForm.getExtensionElements());
}
public Builder setType(Type type) {
this.type = Objects.requireNonNull(type);
return this;
}
/**
* Sets the description of the data. It is similar to the title on a web page or an X window.
* You can put a &lt;title/&gt; on either a form to fill out, or a set of data results.
*
* @param title description of the data.
* @return a reference to this builder.
*/
public Builder setTitle(String title) {
this.title = title;
return this;
}
/**
* Adds a new field as part of the form.
*
* @param field the field to add to the form.
* @return a reference to this builder.
*/
public Builder addField(FormField field) {
String fieldName = field.getFieldName();
if (fieldName != null) {
if (fieldsMap.containsKey(fieldName)) {
throw new IllegalArgumentException("A field with the name " + fieldName + " already exists");
}
fieldsMap.put(fieldName, field);
}
fields.add(field);
return this;
}
/**
* Add the given fields to this form.
*
* @param fieldsToAdd TODO javadoc me please
* @return a reference to this builder.
*/
public Builder addFields(Collection<? extends FormField> fieldsToAdd) {
for (FormField field : fieldsToAdd) {
String fieldName = field.getFieldName();
if (fieldsMap.containsKey(fieldName)) {
throw new IllegalArgumentException("A field with the name " + fieldName + " already exists");
}
}
for (FormField field : fieldsToAdd) {
String fieldName = field.getFieldName();
if (fieldName != null) {
fieldsMap.put(fieldName, field);
}
fields.add(field);
}
return this;
}
public Builder removeField(String fieldName) {
FormField field = fieldsMap.remove(fieldName);
if (field != null) {
fields.remove(field);
}
return this;
}
public Builder setFormType(String formType) {
FormField formField = FormField.buildHiddenFormType(formType);
return addField(formField);
}
public Builder setInstructions(String instructions) {
return setInstructions(StringUtils.splitLinesPortable(instructions));
}
/**
* Sets the list of instructions that explain how to fill out the form and what the form is
* about. The dataform could include multiple instructions since each instruction could not
* contain newlines characters.
*
* @param instructions list of instructions that explain how to fill out the form.
* @return a reference to this builder.
*/
public Builder setInstructions(List<String> instructions) {
this.instructions = instructions;
return this;
}
/**
* Adds a new instruction to the list of instructions that explain how to fill out the form
* and what the form is about. The dataform could include multiple instructions since each
* instruction could not contain newlines characters.
*
* @param instruction the new instruction that explain how to fill out the form.
* @return a reference to this builder.
*/
public Builder addInstruction(String instruction) {
if (instructions == null) {
instructions = new ArrayList<>();
}
instructions.add(instruction);
return this;
}
/**
* Adds a new item returned from a search.
*
* @param item the item returned from a search.
* @return a reference to this builder.
*/
public Builder addItem(Item item) {
items.add(item);
return this;
}
/**
* Sets the fields that will be returned from a search.
*
* @param reportedData the fields that will be returned from a search.
* @return a reference to this builder.
*/
public Builder setReportedData(ReportedData reportedData) {
this.reportedData = reportedData;
return this;
}
public Builder addExtensionElement(Element element) {
if (extensionElements == null) {
extensionElements = new ArrayList<>();
}
extensionElements.add(element);
return this;
}
public DataForm build() {
return new DataForm(this);
}
}
/** /**
* *
* Represents the fields that will be returned from a search. This information is useful when * Represents the fields that will be returned from a search. This information is useful when
@ -436,13 +529,24 @@ public class DataForm implements ExtensionElement {
* *
* @author Gaston Dombiak * @author Gaston Dombiak
*/ */
public static class ReportedData { public static class ReportedData implements ExtensionElement {
public static final String ELEMENT = "reported"; public static final String ELEMENT = "reported";
public static final QName QNAME = new QName(NAMESPACE, ELEMENT);
private List<FormField> fields = new ArrayList<>(); private final List<? extends FormField> fields;
public ReportedData(List<FormField> fields) { public ReportedData(List<? extends FormField> fields) {
this.fields = fields; this.fields = Collections.unmodifiableList(fields);
}
@Override
public String getElementName() {
return ELEMENT;
}
@Override
public String getNamespace() {
return NAMESPACE;
} }
/** /**
@ -450,20 +554,19 @@ public class DataForm implements ExtensionElement {
* *
* @return the fields returned from a search. * @return the fields returned from a search.
*/ */
public List<FormField> getFields() { public List<? extends FormField> getFields() {
return Collections.unmodifiableList(new ArrayList<>(fields)); return fields;
} }
public CharSequence toXML() { @Override
XmlStringBuilder buf = new XmlStringBuilder(); public XmlStringBuilder toXML(XmlEnvironment xmlEnvironment) {
buf.openElement(ELEMENT); XmlStringBuilder xml = new XmlStringBuilder(this, xmlEnvironment);
// Loop through all the form items and append them to the string buffer xml.rightAngleBracket();
for (FormField field : getFields()) { xml.append(getFields());
buf.append(field.toXML()); xml.closeElement(this);
} return xml;
buf.closeElement(ELEMENT);
return buf;
} }
} }
/** /**
@ -472,13 +575,24 @@ public class DataForm implements ExtensionElement {
* *
* @author Gaston Dombiak * @author Gaston Dombiak
*/ */
public static class Item { public static class Item implements ExtensionElement {
public static final String ELEMENT = "item"; public static final String ELEMENT = "item";
public static final QName QNAME = new QName(NAMESPACE, ELEMENT);
private List<FormField> fields = new ArrayList<>(); private final List<? extends FormField> fields;
public Item(List<FormField> fields) { public Item(List<? extends FormField> fields) {
this.fields = fields; this.fields = Collections.unmodifiableList(fields);
}
@Override
public String getElementName() {
return ELEMENT;
}
@Override
public String getNamespace() {
return NAMESPACE;
} }
/** /**
@ -486,19 +600,17 @@ public class DataForm implements ExtensionElement {
* *
* @return the fields that define the data that goes with the item. * @return the fields that define the data that goes with the item.
*/ */
public List<FormField> getFields() { public List<? extends FormField> getFields() {
return Collections.unmodifiableList(new ArrayList<>(fields)); return fields;
} }
public CharSequence toXML() { @Override
XmlStringBuilder buf = new XmlStringBuilder(); public XmlStringBuilder toXML(XmlEnvironment xmlEnvironment) {
buf.openElement(ELEMENT); XmlStringBuilder xml = new XmlStringBuilder(this, xmlEnvironment);
// Loop through all the form items and append them to the string buffer xml.rightAngleBracket();
for (FormField field : getFields()) { xml.append(getFields());
buf.append(field.toXML()); xml.closeElement(this);
} return xml;
buf.closeElement(ELEMENT);
return buf;
} }
} }
} }

View file

@ -1,6 +1,6 @@
/** /**
* *
* Copyright 2003-2007 Jive Software. * Copyright 2003-2007 Jive Software 2020 Florian Schmaus.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -19,6 +19,7 @@ package org.jivesoftware.smackx.xdata.provider;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -29,16 +30,28 @@ import org.jivesoftware.smack.parsing.SmackParsingException;
import org.jivesoftware.smack.provider.ExtensionElementProvider; import org.jivesoftware.smack.provider.ExtensionElementProvider;
import org.jivesoftware.smack.roster.packet.RosterPacket; import org.jivesoftware.smack.roster.packet.RosterPacket;
import org.jivesoftware.smack.roster.provider.RosterPacketProvider; import org.jivesoftware.smack.roster.provider.RosterPacketProvider;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smack.xml.XmlPullParser; import org.jivesoftware.smack.xml.XmlPullParser;
import org.jivesoftware.smack.xml.XmlPullParserException; import org.jivesoftware.smack.xml.XmlPullParserException;
import org.jivesoftware.smackx.formtypes.FormFieldRegistry;
import org.jivesoftware.smackx.xdata.AbstractMultiFormField;
import org.jivesoftware.smackx.xdata.AbstractSingleStringValueFormField;
import org.jivesoftware.smackx.xdata.BooleanFormField;
import org.jivesoftware.smackx.xdata.FormField; import org.jivesoftware.smackx.xdata.FormField;
import org.jivesoftware.smackx.xdata.FormFieldChildElement; import org.jivesoftware.smackx.xdata.FormFieldChildElement;
import org.jivesoftware.smackx.xdata.FormFieldWithOptions;
import org.jivesoftware.smackx.xdata.JidMultiFormField;
import org.jivesoftware.smackx.xdata.JidSingleFormField;
import org.jivesoftware.smackx.xdata.ListMultiFormField;
import org.jivesoftware.smackx.xdata.ListSingleFormField;
import org.jivesoftware.smackx.xdata.TextSingleFormField;
import org.jivesoftware.smackx.xdata.packet.DataForm; import org.jivesoftware.smackx.xdata.packet.DataForm;
import org.jivesoftware.smackx.xdatalayout.packet.DataLayout; import org.jivesoftware.smackx.xdatalayout.packet.DataLayout;
import org.jivesoftware.smackx.xdatalayout.provider.DataLayoutProvider; import org.jivesoftware.smackx.xdatalayout.provider.DataLayoutProvider;
import org.jxmpp.jid.Jid;
import org.jxmpp.jid.impl.JidCreate;
/** /**
* The DataFormProvider parses DataForm packets. * The DataFormProvider parses DataForm packets.
* *
@ -53,7 +66,11 @@ public class DataFormProvider extends ExtensionElementProvider<DataForm> {
@Override @Override
public DataForm parse(XmlPullParser parser, int initialDepth, XmlEnvironment xmlEnvironment) throws XmlPullParserException, IOException, SmackParsingException { public DataForm parse(XmlPullParser parser, int initialDepth, XmlEnvironment xmlEnvironment) throws XmlPullParserException, IOException, SmackParsingException {
DataForm.Type dataFormType = DataForm.Type.fromString(parser.getAttributeValue("", "type")); DataForm.Type dataFormType = DataForm.Type.fromString(parser.getAttributeValue("", "type"));
DataForm dataForm = new DataForm(dataFormType); DataForm.Builder dataForm = DataForm.builder();
dataForm.setType(dataFormType);
String formType = null;
outerloop: while (true) { outerloop: while (true) {
XmlPullParser.Event eventType = parser.next(); XmlPullParser.Event eventType = parser.next();
switch (eventType) { switch (eventType) {
@ -69,15 +86,24 @@ public class DataFormProvider extends ExtensionElementProvider<DataForm> {
dataForm.setTitle(parser.nextText()); dataForm.setTitle(parser.nextText());
break; break;
case "field": case "field":
FormField formField = parseField(parser, elementXmlEnvironment); FormField formField = parseField(parser, elementXmlEnvironment, formType, dataFormType);
TextSingleFormField hiddenFormTypeField = formField.asHiddenFormTypeFieldIfPossible();
if (hiddenFormTypeField != null) {
if (formType != null) {
throw new SmackParsingException("Multiple hidden form type fields");
}
formType = hiddenFormTypeField.getValue();
}
dataForm.addField(formField); dataForm.addField(formField);
break; break;
case "item": case "item":
DataForm.Item item = parseItem(parser, elementXmlEnvironment); DataForm.Item item = parseItem(parser, elementXmlEnvironment, formType, dataFormType);
dataForm.addItem(item); dataForm.addItem(item);
break; break;
case "reported": case "reported":
DataForm.ReportedData reported = parseReported(parser, elementXmlEnvironment); DataForm.ReportedData reported = parseReported(parser, elementXmlEnvironment, formType, dataFormType);
dataForm.setReportedData(reported); dataForm.setReportedData(reported);
break; break;
// See XEP-133 Example 32 for a corner case where the data form contains this extension. // See XEP-133 Example 32 for a corner case where the data form contains this extension.
@ -104,38 +130,52 @@ public class DataFormProvider extends ExtensionElementProvider<DataForm> {
break; break;
} }
} }
return dataForm; return dataForm.build();
} }
private static FormField parseField(XmlPullParser parser, XmlEnvironment xmlEnvironment) private static FormField parseField(XmlPullParser parser, XmlEnvironment xmlEnvironment, String formType, DataForm.Type dataFormType)
throws XmlPullParserException, IOException, SmackParsingException { throws XmlPullParserException, IOException, SmackParsingException {
final int initialDepth = parser.getDepth(); final int initialDepth = parser.getDepth();
final String var = parser.getAttributeValue("", "var"); final String fieldName = parser.getAttributeValue("var");
final FormField.Type type = FormField.Type.fromString(parser.getAttributeValue("", "type")); final String label = parser.getAttributeValue("", "label");
final FormField.Builder builder = FormField.builder(); FormField.Type type = null;
builder.setType(type); {
if (type != FormField.Type.fixed) { String fieldTypeString = parser.getAttributeValue("type");
builder.setVariable(var); if (fieldTypeString != null) {
type = FormField.Type.fromString(fieldTypeString);
} }
String label = parser.getAttributeValue("", "label");
if (StringUtils.isNotEmpty(label)) {
builder.setLabel(label);
} }
List<FormField.Value> values = new ArrayList<>();
List<FormField.Option> options = new ArrayList<>();
List<FormFieldChildElement> childElements = new ArrayList<>();
boolean required = false;
outerloop: while (true) { outerloop: while (true) {
XmlPullParser.Event eventType = parser.next(); XmlPullParser.TagEvent eventType = parser.nextTag();
switch (eventType) { switch (eventType) {
case START_ELEMENT: case START_ELEMENT:
QName qname = parser.getQName(); QName qname = parser.getQName();
if (qname.equals(FormField.Value.QNAME)) {
FormField.Value value = parseValue(parser);
values.add(value);
} else if (qname.equals(FormField.Option.QNAME)) {
FormField.Option option = parseOption(parser);
options.add(option);
} else if (qname.equals(FormField.Required.QNAME)) {
required = true;
} else {
FormFieldChildElementProvider<?> provider = FormFieldChildElementProviderManager.getFormFieldChildElementProvider( FormFieldChildElementProvider<?> provider = FormFieldChildElementProviderManager.getFormFieldChildElementProvider(
qname); qname);
if (provider != null) { if (provider == null) {
FormFieldChildElement formFieldChildElement = provider.parse(parser, XmlEnvironment.from(parser, xmlEnvironment));
builder.addFormFieldChildElement(formFieldChildElement);
} else {
LOGGER.warning("Unknown form field child element " + qname + " ignored"); LOGGER.warning("Unknown form field child element " + qname + " ignored");
continue;
}
FormFieldChildElement formFieldChildElement = provider.parse(parser,
XmlEnvironment.from(parser, xmlEnvironment));
childElements.add(formFieldChildElement);
} }
break; break;
case END_ELEMENT: case END_ELEMENT:
@ -143,16 +183,128 @@ public class DataFormProvider extends ExtensionElementProvider<DataForm> {
break outerloop; break outerloop;
} }
break; break;
}
}
if (type == null) {
if (dataFormType == DataForm.Type.submit) {
// If the data form is of type submit, and no type was explicitly provided, then we need to lookup the
// field's type in the registry.
type = FormFieldRegistry.lookup(formType, fieldName);
if (type == null) {
throw new SmackParsingException("Field of name '" + fieldName + "' (and FORM_TYPE '" + formType
+ "') not registered");
}
} else {
// As per XEP-0004, text-single is the default form field type.
type = FormField.Type.text_single;
}
}
FormField.Builder<?, ?> builder;
switch (type) {
case bool:
builder = parseBooleanFormField(fieldName, values);
break;
case fixed:
builder = parseSingleKindFormField(FormField.fixedBuilder(fieldName), values);
break;
case hidden:
builder = parseSingleKindFormField(FormField.hiddenBuilder(fieldName), values);
break;
case jid_multi:
JidMultiFormField.Builder jidMultiBuilder = FormField.jidMultiBuilder(fieldName);
for (FormField.Value value : values) {
Jid jid = JidCreate.from(value.getValue());
jidMultiBuilder.addValue(jid);
}
builder = jidMultiBuilder;
break;
case jid_single:
ensureAtMostSingleValue(type, values);
JidSingleFormField.Builder jidSingleBuilder = FormField.jidSingleBuilder(fieldName);
if (!values.isEmpty()) {
CharSequence jidCharSequence = values.get(0).getValue();
Jid jid = JidCreate.from(jidCharSequence);
jidSingleBuilder.setValue(jid);
}
builder = jidSingleBuilder;
break;
case list_multi:
ListMultiFormField.Builder listMultiBuilder = FormField.listMultiBuilder(fieldName);
addOptionsToBuilder(options, listMultiBuilder);
builder = parseMultiKindFormField(listMultiBuilder, values);
break;
case list_single:
ListSingleFormField.Builder listSingleBuilder = FormField.listSingleBuilder(fieldName);
addOptionsToBuilder(options, listSingleBuilder);
builder = parseSingleKindFormField(listSingleBuilder, values);
break;
case text_multi:
builder = parseMultiKindFormField(FormField.textMultiBuilder(fieldName), values);
break;
case text_private:
builder = parseSingleKindFormField(FormField.textPrivateBuilder(fieldName), values);
break;
case text_single:
builder = parseSingleKindFormField(FormField.textSingleBuilder(fieldName), values);
break;
default: default:
// Catch all for incomplete switch (MissingCasesInEnumSwitch) statement. // Should never happen, as we cover all types in the switch/case.
throw new AssertionError("Unknown type " + type);
}
switch (type) {
case list_multi:
case list_single:
break;
default:
if (!options.isEmpty()) {
throw new SmackParsingException("Form fields of type " + type + " must not have options. This one had "
+ options.size());
}
break; break;
} }
if (label != null) {
builder.setLabel(label);
} }
builder.setRequired(required);
builder.addFormFieldChildElements(childElements);
return builder.build(); return builder.build();
} }
private static DataForm.Item parseItem(XmlPullParser parser, XmlEnvironment xmlEnvironment) private static FormField.Builder<?, ?> parseBooleanFormField(String fieldName, List<FormField.Value> values) throws SmackParsingException {
BooleanFormField.Builder builder = FormField.booleanBuilder(fieldName);
if (values.size() == 1) {
String value = values.get(0).getValue().toString();
builder.setValue(value);
}
return builder;
}
private static AbstractSingleStringValueFormField.Builder<?, ?> parseSingleKindFormField(
AbstractSingleStringValueFormField.Builder<?, ?> builder, List<FormField.Value> values)
throws SmackParsingException {
ensureAtMostSingleValue(builder.getType(), values);
if (values.size() == 1) {
String value = values.get(0).getValue().toString();
builder.setValue(value);
}
return builder;
}
private static AbstractMultiFormField.Builder<?, ?> parseMultiKindFormField(AbstractMultiFormField.Builder<?, ?> builder,
List<FormField.Value> values) {
for (FormField.Value value : values) {
builder.addValue(value.getValue());
}
return builder;
}
private static DataForm.Item parseItem(XmlPullParser parser, XmlEnvironment xmlEnvironment, String formType, DataForm.Type dataFormType)
throws XmlPullParserException, IOException, SmackParsingException { throws XmlPullParserException, IOException, SmackParsingException {
final int initialDepth = parser.getDepth(); final int initialDepth = parser.getDepth();
List<FormField> fields = new ArrayList<>(); List<FormField> fields = new ArrayList<>();
@ -163,7 +315,7 @@ public class DataFormProvider extends ExtensionElementProvider<DataForm> {
String name = parser.getName(); String name = parser.getName();
switch (name) { switch (name) {
case "field": case "field":
FormField field = parseField(parser, XmlEnvironment.from(parser, xmlEnvironment)); FormField field = parseField(parser, XmlEnvironment.from(parser, xmlEnvironment), formType, dataFormType);
fields.add(field); fields.add(field);
break; break;
} }
@ -178,7 +330,7 @@ public class DataFormProvider extends ExtensionElementProvider<DataForm> {
return new DataForm.Item(fields); return new DataForm.Item(fields);
} }
private static DataForm.ReportedData parseReported(XmlPullParser parser, XmlEnvironment xmlEnvironment) private static DataForm.ReportedData parseReported(XmlPullParser parser, XmlEnvironment xmlEnvironment, String formType, DataForm.Type dataFormType)
throws XmlPullParserException, IOException, SmackParsingException { throws XmlPullParserException, IOException, SmackParsingException {
final int initialDepth = parser.getDepth(); final int initialDepth = parser.getDepth();
List<FormField> fields = new ArrayList<>(); List<FormField> fields = new ArrayList<>();
@ -189,7 +341,7 @@ public class DataFormProvider extends ExtensionElementProvider<DataForm> {
String name = parser.getName(); String name = parser.getName();
switch (name) { switch (name) {
case "field": case "field":
FormField field = parseField(parser, XmlEnvironment.from(parser, xmlEnvironment)); FormField field = parseField(parser, XmlEnvironment.from(parser, xmlEnvironment), formType, dataFormType);
fields.add(field); fields.add(field);
break; break;
} }
@ -204,4 +356,45 @@ public class DataFormProvider extends ExtensionElementProvider<DataForm> {
return new DataForm.ReportedData(fields); return new DataForm.ReportedData(fields);
} }
public static FormField.Value parseValue(XmlPullParser parser) throws IOException, XmlPullParserException {
String value = parser.nextText();
return new FormField.Value(value);
}
public static FormField.Option parseOption(XmlPullParser parser) throws IOException, XmlPullParserException {
int initialDepth = parser.getDepth();
FormField.Option option = null;
String label = parser.getAttributeValue("", "label");
outerloop: while (true) {
XmlPullParser.TagEvent eventType = parser.nextTag();
switch (eventType) {
case START_ELEMENT:
String name = parser.getName();
switch (name) {
case "value":
option = new FormField.Option(label, parser.nextText());
break;
}
break;
case END_ELEMENT:
if (parser.getDepth() == initialDepth) {
break outerloop;
}
break;
}
}
return option;
}
private static void ensureAtMostSingleValue(FormField.Type type, List<FormField.Value> values) throws SmackParsingException {
if (values.size() > 1) {
throw new SmackParsingException(type + " fields can have at most one value, this one had " + values.size());
}
}
private static void addOptionsToBuilder(Collection<FormField.Option> options, FormFieldWithOptions.Builder<?> builder) {
for (FormField.Option option : options) {
builder.addOption(option);
}
}
} }

View file

@ -1,63 +0,0 @@
/**
*
* Copyright 2019 Florian Schmaus
*
* 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.xdata.provider;
import java.io.IOException;
import javax.xml.namespace.QName;
import org.jivesoftware.smack.packet.XmlEnvironment;
import org.jivesoftware.smack.parsing.SmackParsingException;
import org.jivesoftware.smack.xml.XmlPullParser;
import org.jivesoftware.smack.xml.XmlPullParserException;
import org.jivesoftware.smackx.xdata.FormField;
public class OptionProvider extends FormFieldChildElementProvider<FormField.Option> {
@Override
public FormField.Option parse(XmlPullParser parser, int initialDepth, XmlEnvironment xmlEnvironment)
throws XmlPullParserException, IOException, SmackParsingException {
FormField.Option option = null;
String label = parser.getAttributeValue("", "label");
outerloop: while (true) {
XmlPullParser.TagEvent eventType = parser.nextTag();
switch (eventType) {
case START_ELEMENT:
String name = parser.getName();
switch (name) {
case "value":
option = new FormField.Option(label, parser.nextText());
break;
}
break;
case END_ELEMENT:
if (parser.getDepth() == initialDepth) {
break outerloop;
}
break;
}
}
return option;
}
@Override
public QName getQName() {
return FormField.Option.QNAME;
}
}

View file

@ -1,43 +0,0 @@
/**
*
* Copyright 2019 Florian Schmaus
*
* 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.xdata.provider;
import java.io.IOException;
import javax.xml.namespace.QName;
import org.jivesoftware.smack.packet.XmlEnvironment;
import org.jivesoftware.smack.parsing.SmackParsingException;
import org.jivesoftware.smack.xml.XmlPullParser;
import org.jivesoftware.smack.xml.XmlPullParserException;
import org.jivesoftware.smackx.xdata.FormField;
public class RequiredProvider extends FormFieldChildElementProvider<FormField.Required> {
@Override
public FormField.Required parse(XmlPullParser parser, int initialDepth, XmlEnvironment xmlEnvironment)
throws XmlPullParserException, IOException, SmackParsingException {
return FormField.Required.INSTANCE;
}
@Override
public QName getQName() {
return FormField.Required.QNAME;
}
}

View file

@ -1,43 +0,0 @@
/**
*
* Copyright 2019 Florian Schmaus
*
* 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.xdata.provider;
import java.io.IOException;
import javax.xml.namespace.QName;
import org.jivesoftware.smack.packet.XmlEnvironment;
import org.jivesoftware.smack.xml.XmlPullParser;
import org.jivesoftware.smack.xml.XmlPullParserException;
import org.jivesoftware.smackx.xdata.FormField;
public class ValueProvider extends FormFieldChildElementProvider<FormField.Value> {
@Override
public FormField.Value parse(XmlPullParser parser, int initialDepth, XmlEnvironment xmlEnvironment)
throws XmlPullParserException, IOException {
String value = parser.nextText();
return new FormField.Value(value);
}
@Override
public QName getQName() {
return FormField.Value.QNAME;
}
}

View file

@ -1,6 +1,6 @@
/** /**
* *
* Copyright 2014 Anno van Vliet, 2019 Florian Schmaus * Copyright 2014 Anno van Vliet, 2019-2020 Florian Schmaus
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,6 +16,8 @@
*/ */
package org.jivesoftware.smackx.xdatavalidation.packet; package org.jivesoftware.smackx.xdatavalidation.packet;
import java.math.BigInteger;
import javax.xml.namespace.QName; import javax.xml.namespace.QName;
import org.jivesoftware.smack.datatypes.UInt32; import org.jivesoftware.smack.datatypes.UInt32;
@ -24,6 +26,7 @@ import org.jivesoftware.smack.packet.XmlEnvironment;
import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smack.util.XmlStringBuilder; import org.jivesoftware.smack.util.XmlStringBuilder;
import org.jivesoftware.smackx.xdata.AbstractSingleStringValueFormField;
import org.jivesoftware.smackx.xdata.FormField; import org.jivesoftware.smackx.xdata.FormField;
import org.jivesoftware.smackx.xdata.FormFieldChildElement; import org.jivesoftware.smackx.xdata.FormFieldChildElement;
import org.jivesoftware.smackx.xdata.packet.DataForm; import org.jivesoftware.smackx.xdata.packet.DataForm;
@ -135,7 +138,7 @@ public abstract class ValidateElement implements FormFieldChildElement {
* @param formFieldBuilder the builder used to construct the form field. * @param formFieldBuilder the builder used to construct the form field.
*/ */
@Override @Override
public abstract void checkConsistency(FormField.Builder formFieldBuilder); public abstract void checkConsistency(FormField.Builder<?, ?> formFieldBuilder);
public static ValidateElement from(FormField formField) { public static ValidateElement from(FormField formField) {
return (ValidateElement) formField.getFormFieldChildElement(QNAME); return (ValidateElement) formField.getFormFieldChildElement(QNAME);
@ -166,7 +169,7 @@ public abstract class ValidateElement implements FormFieldChildElement {
} }
@Override @Override
public void checkConsistency(FormField.Builder formField) { public void checkConsistency(FormField.Builder<?, ?> formField) {
checkListRangeConsistency(formField); checkListRangeConsistency(formField);
if (formField.getType() != null) { if (formField.getType() != null) {
switch (formField.getType()) { switch (formField.getType()) {
@ -209,7 +212,7 @@ public abstract class ValidateElement implements FormFieldChildElement {
} }
@Override @Override
public void checkConsistency(FormField.Builder formField) { public void checkConsistency(FormField.Builder<?, ?> formField) {
checkListRangeConsistency(formField); checkListRangeConsistency(formField);
if (formField.getType() != null) { if (formField.getType() != null) {
switch (formField.getType()) { switch (formField.getType()) {
@ -277,7 +280,7 @@ public abstract class ValidateElement implements FormFieldChildElement {
} }
@Override @Override
public void checkConsistency(FormField.Builder formField) { public void checkConsistency(FormField.Builder<?, ?> formField) {
checkNonMultiConsistency(formField, METHOD); checkNonMultiConsistency(formField, METHOD);
if (getDatatype().equals(ValidateElement.DATATYPE_XS_STRING)) { if (getDatatype().equals(ValidateElement.DATATYPE_XS_STRING)) {
throw new ValidationConsistencyException(String.format( throw new ValidationConsistencyException(String.format(
@ -286,6 +289,38 @@ public abstract class ValidateElement implements FormFieldChildElement {
} }
} }
@Override
public void validate(FormField formField) {
AbstractSingleStringValueFormField singleValueFormField = formField.ifPossibleAs(AbstractSingleStringValueFormField.class);
if (singleValueFormField == null) {
// We currently only implement validation for single value fields.
return;
}
String valueString = singleValueFormField.getValue();
switch (getDatatype()) {
case "xs:int":
case "xs:integer":
BigInteger value = new BigInteger(valueString);
String minString = getMin();
if (minString != null) {
BigInteger min = new BigInteger(minString);
if (value.compareTo(min) < 0) {
throw new IllegalArgumentException("The provided value " + valueString + " is lower than the allowed minimum of " + minString);
}
}
String maxString = getMax();
if (maxString != null) {
BigInteger max = new BigInteger(maxString);
if (value.compareTo(max) > 0) {
throw new IllegalArgumentException("The provided value " + valueString + " is higher than the allowed maximum of " + maxString);
}
}
break;
}
}
} }
/** /**
@ -327,7 +362,7 @@ public abstract class ValidateElement implements FormFieldChildElement {
} }
@Override @Override
public void checkConsistency(FormField.Builder formField) { public void checkConsistency(FormField.Builder<?, ?> formField) {
checkNonMultiConsistency(formField, METHOD); checkNonMultiConsistency(formField, METHOD);
} }
@ -408,7 +443,7 @@ public abstract class ValidateElement implements FormFieldChildElement {
* *
* @param formField TODO javadoc me please * @param formField TODO javadoc me please
*/ */
protected void checkListRangeConsistency(FormField.Builder formField) { protected void checkListRangeConsistency(FormField.Builder<?, ?> formField) {
ListRange listRange = getListRange(); ListRange listRange = getListRange();
if (listRange == null) { if (listRange == null) {
return; return;
@ -426,7 +461,7 @@ public abstract class ValidateElement implements FormFieldChildElement {
* @param formField TODO javadoc me please * @param formField TODO javadoc me please
* @param method TODO javadoc me please * @param method TODO javadoc me please
*/ */
protected void checkNonMultiConsistency(FormField.Builder formField, String method) { protected void checkNonMultiConsistency(FormField.Builder<?, ?> formField, String method) {
checkListRangeConsistency(formField); checkListRangeConsistency(formField);
if (formField.getType() != null) { if (formField.getType() != null) {
switch (formField.getType()) { switch (formField.getType()) {

View file

@ -36,6 +36,8 @@ import org.jivesoftware.smackx.caps.cache.SimpleDirectoryPersistentCache;
import org.jivesoftware.smackx.disco.packet.DiscoverInfo; import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
import org.jivesoftware.smackx.disco.packet.DiscoverInfoBuilder; import org.jivesoftware.smackx.disco.packet.DiscoverInfoBuilder;
import org.jivesoftware.smackx.xdata.FormField; import org.jivesoftware.smackx.xdata.FormField;
import org.jivesoftware.smackx.xdata.TextMultiFormField;
import org.jivesoftware.smackx.xdata.TextSingleFormField;
import org.jivesoftware.smackx.xdata.packet.DataForm; import org.jivesoftware.smackx.xdata.packet.DataForm;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -97,6 +99,49 @@ public class EntityCapsManagerTest extends SmackTestSuite {
assertEquals(di.toXML().toString(), restored_di.toXML().toString()); assertEquals(di.toXML().toString(), restored_di.toXML().toString());
} }
private static DataForm createSampleSoftwareInfoDataForm() {
DataForm.Builder df = DataForm.builder(DataForm.Type.result);
{
TextSingleFormField.Builder ff = FormField.builder("os");
ff.setValue("Mac");
df.addField(ff.build());
}
{
TextSingleFormField.Builder ff = FormField.hiddenBuilder("FORM_TYPE");
ff.setValue("urn:xmpp:dataforms:softwareinfo");
df.addField(ff.build());
}
{
TextMultiFormField.Builder ff = FormField.textMultiBuilder("ip_version");
ff.addValue("ipv4");
ff.addValue("ipv6");
df.addField(ff.build());
}
{
TextSingleFormField.Builder ff = FormField.builder("os_version");
ff.setValue("10.5.1");
df.addField(ff.build());
}
{
TextSingleFormField.Builder ff = FormField.builder("software");
ff.setValue("Psi");
df.addField(ff.build());
}
{
TextSingleFormField.Builder ff = FormField.builder("software_version");
ff.setValue("0.11");
df.addField(ff.build());
}
return df.build();
}
private static DiscoverInfo createComplexSamplePacket() throws XmppStringprepException { private static DiscoverInfo createComplexSamplePacket() throws XmppStringprepException {
DiscoverInfoBuilder di = DiscoverInfo.builder("disco1"); DiscoverInfoBuilder di = DiscoverInfo.builder("disco1");
di.from(JidCreate.from("benvolio@capulet.lit/230193")); di.from(JidCreate.from("benvolio@capulet.lit/230193"));
@ -115,35 +160,8 @@ public class EntityCapsManagerTest extends SmackTestSuite {
di.addFeature("http://jabber.org/protocol/muc"); di.addFeature("http://jabber.org/protocol/muc");
di.addFeature("http://jabber.org/protocol/disco#info"); di.addFeature("http://jabber.org/protocol/disco#info");
DataForm df = new DataForm(DataForm.Type.result); DataForm softwareInfoDataForm = createSampleSoftwareInfoDataForm();
di.addExtension(softwareInfoDataForm);
FormField.Builder ff = FormField.builder("os");
ff.addValue("Mac");
df.addField(ff.build());
ff = FormField.builder("FORM_TYPE");
ff.setType(FormField.Type.hidden);
ff.addValue("urn:xmpp:dataforms:softwareinfo");
df.addField(ff.build());
ff = FormField.builder("ip_version");
ff.addValue("ipv4");
ff.addValue("ipv6");
df.addField(ff.build());
ff = FormField.builder("os_version");
ff.addValue("10.5.1");
df.addField(ff.build());
ff = FormField.builder("software");
ff.addValue("Psi");
df.addField(ff.build());
ff = FormField.builder("software_version");
ff.addValue("0.11");
df.addField(ff.build());
di.addExtension(df);
return di.build(); return di.build();
} }
@ -171,50 +189,8 @@ public class EntityCapsManagerTest extends SmackTestSuite {
// Failure 2: Duplicate features // Failure 2: Duplicate features
di.addFeature("http://jabber.org/protocol/disco#info"); di.addFeature("http://jabber.org/protocol/disco#info");
DataForm df = new DataForm(DataForm.Type.result); DataForm softwareInfoDataForm = createSampleSoftwareInfoDataForm();
di.addExtension(softwareInfoDataForm);
FormField.Builder ff = FormField.builder("os");
ff.addValue("Mac");
df.addField(ff.build());
ff = FormField.builder("FORM_TYPE");
ff.setType(FormField.Type.hidden);
ff.addValue("urn:xmpp:dataforms:softwareinfo");
df.addField(ff.build());
ff = FormField.builder("ip_version");
ff.addValue("ipv4");
ff.addValue("ipv6");
df.addField(ff.build());
ff = FormField.builder("os_version");
ff.addValue("10.5.1");
df.addField(ff.build());
ff = FormField.builder("software");
ff.addValue("Psi");
df.addField(ff.build());
ff = FormField.builder("software_version");
ff.addValue("0.11");
df.addField(ff.build());
di.addExtension(df);
// Failure 3: Another service discovery information form with the same
// FORM_TYPE
df = new DataForm(DataForm.Type.result);
ff = FormField.builder("FORM_TYPE");
ff.setType(FormField.Type.hidden);
ff.addValue("urn:xmpp:dataforms:softwareinfo");
df.addField(ff.build());
ff = FormField.builder("software");
ff.addValue("smack");
df.addField(ff.build());
di.addExtension(df);
DiscoverInfo discoverInfo = di.buildWithoutValidiation(); DiscoverInfo discoverInfo = di.buildWithoutValidiation();
return discoverInfo; return discoverInfo;

View file

@ -21,6 +21,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import org.jivesoftware.smackx.disco.packet.DiscoverInfo; import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
import org.jivesoftware.smackx.xdata.FormField; import org.jivesoftware.smackx.xdata.FormField;
import org.jivesoftware.smackx.xdata.TextSingleFormField;
import org.jivesoftware.smackx.xdata.packet.DataForm; import org.jivesoftware.smackx.xdata.packet.DataForm;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -28,7 +29,7 @@ import org.junit.jupiter.api.Test;
public class RoomInfoTest { public class RoomInfoTest {
@Test @Test
public void validateRoomWithEmptyForm() { public void validateRoomWithEmptyForm() {
DataForm dataForm = new DataForm(DataForm.Type.result); DataForm dataForm = DataForm.builder(DataForm.Type.result).build();
DiscoverInfo discoInfo = DiscoverInfo.builder("disco1") DiscoverInfo discoInfo = DiscoverInfo.builder("disco1")
.addExtension(dataForm) .addExtension(dataForm)
@ -41,22 +42,22 @@ public class RoomInfoTest {
@Test @Test
public void validateRoomWithForm() { public void validateRoomWithForm() {
DataForm dataForm = new DataForm(DataForm.Type.result); DataForm.Builder dataForm = DataForm.builder(DataForm.Type.result);
FormField.Builder desc = FormField.builder("muc#roominfo_description"); TextSingleFormField.Builder desc = FormField.builder("muc#roominfo_description");
desc.addValue("The place for all good witches!"); desc.setValue("The place for all good witches!");
dataForm.addField(desc.build()); dataForm.addField(desc.build());
FormField.Builder subject = FormField.builder("muc#roominfo_subject"); TextSingleFormField.Builder subject = FormField.builder("muc#roominfo_subject");
subject.addValue("Spells"); subject.setValue("Spells");
dataForm.addField(subject.build()); dataForm.addField(subject.build());
FormField.Builder occupants = FormField.builder("muc#roominfo_occupants"); TextSingleFormField.Builder occupants = FormField.builder("muc#roominfo_occupants");
occupants.addValue("3"); occupants.setValue("3");
dataForm.addField(occupants.build()); dataForm.addField(occupants.build());
DiscoverInfo discoInfo = DiscoverInfo.builder("disco1") DiscoverInfo discoInfo = DiscoverInfo.builder("disco1")
.addExtension(dataForm) .addExtension(dataForm.build())
.build(); .build();
RoomInfo roomInfo = new RoomInfo(discoInfo); RoomInfo roomInfo = new RoomInfo(discoInfo);
assertEquals("The place for all good witches!", roomInfo.getDescription()); assertEquals("The place for all good witches!", roomInfo.getDescription());

View file

@ -37,7 +37,6 @@ import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
import org.jivesoftware.smackx.disco.packet.DiscoverInfo.Identity; import org.jivesoftware.smackx.disco.packet.DiscoverInfo.Identity;
import org.jivesoftware.smackx.disco.packet.DiscoverInfoBuilder; import org.jivesoftware.smackx.disco.packet.DiscoverInfoBuilder;
import org.jivesoftware.smackx.pubsub.packet.PubSub; import org.jivesoftware.smackx.pubsub.packet.PubSub;
import org.jivesoftware.smackx.xdata.packet.DataForm;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -47,12 +46,6 @@ import org.junit.jupiter.api.Test;
* *
*/ */
public class ConfigureFormTest extends SmackTestSuite { public class ConfigureFormTest extends SmackTestSuite {
@Test
public void checkChildrenAssocPolicy() {
ConfigureForm form = new ConfigureForm(DataForm.Type.submit);
form.setChildrenAssociationPolicy(ChildrenAssociationPolicy.owners);
assertEquals(ChildrenAssociationPolicy.owners, form.getChildrenAssociationPolicy());
}
@Test @Test
public void getConfigFormWithInsufficientPrivileges() throws XMPPException, SmackException, IOException, InterruptedException { public void getConfigFormWithInsufficientPrivileges() throws XMPPException, SmackException, IOException, InterruptedException {
@ -107,13 +100,4 @@ public class ConfigureFormTest extends SmackTestSuite {
}); });
} }
@Test
public void checkNotificationType() {
ConfigureForm form = new ConfigureForm(DataForm.Type.submit);
form.setNotificationType(NotificationType.normal);
assertEquals(NotificationType.normal, form.getNotificationType());
form.setNotificationType(NotificationType.headline);
assertEquals(NotificationType.headline, form.getNotificationType());
}
} }

View file

@ -0,0 +1,38 @@
/**
*
* Copyright 2020 Florian Schmaus.
*
* 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.xdata;
import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar;
import org.junit.jupiter.api.Test;
import org.jxmpp.jid.JidTestUtil;
class FormFieldTest {
@Test
public void testJidMultiToXml() {
JidMultiFormField jidMultiFormField = FormField.jidMultiBuilder("myfield")
.addValue(JidTestUtil.BARE_JID_1)
.addValue(JidTestUtil.BARE_JID_2)
.build();
String expectedXml = "<field xmlns='jabber:x:data' var='myfield' type='jid-multi'><value>one@exampleone.org</value><value>one@exampletwo.org</value></field>";
CharSequence xml = jidMultiFormField.toXML();
assertXmlSimilar(expectedXml, xml);
}
}

View file

@ -43,43 +43,42 @@ import org.junit.jupiter.api.Test;
* *
*/ */
public class DataFormTest extends SmackTestSuite { public class DataFormTest extends SmackTestSuite {
private static final String TEST_OUTPUT_1 = "<x xmlns='jabber:x:data' type='submit'><instructions>InstructionTest1</instructions><field var='testField1'/></x>"; private static final String TEST_OUTPUT_1 = "<x xmlns='jabber:x:data' type='form'><instructions>InstructionTest1</instructions><field var='testField1'/></x>";
private static final String TEST_OUTPUT_2 = "<x xmlns='jabber:x:data' type='submit'><instructions>InstructionTest1</instructions><field var='testField1'/><page xmlns='http://jabber.org/protocol/xdata-layout' label='Label'><fieldref var='testField1'/><section label='section Label'><text>SectionText</text></section><text>PageText</text></page></x>"; private static final String TEST_OUTPUT_2 = "<x xmlns='jabber:x:data' type='form'><instructions>InstructionTest1</instructions><field var='testField1'/><page xmlns='http://jabber.org/protocol/xdata-layout' label='Label'><fieldref var='testField1'/><section label='section Label'><text>SectionText</text></section><text>PageText</text></page></x>";
private static final String TEST_OUTPUT_3 = "<x xmlns='jabber:x:data' type='submit'><instructions>InstructionTest1</instructions><field var='testField1'><validate xmlns='http://jabber.org/protocol/xdata-validate' datatype='xs:integer'><range min='1111' max='9999'/></validate></field></x>"; private static final String TEST_OUTPUT_3 = "<x xmlns='jabber:x:data' type='form'><instructions>InstructionTest1</instructions><field var='testField1'><validate xmlns='http://jabber.org/protocol/xdata-validate' datatype='xs:integer'><range min='1111' max='9999'/></validate></field></x>";
private static final DataFormProvider pr = new DataFormProvider(); private static final DataFormProvider pr = new DataFormProvider();
@Test @Test
public void test() throws Exception { public void test() throws Exception {
// Build a Form. // Build a Form.
DataForm df = new DataForm(DataForm.Type.submit); DataForm.Builder df = DataForm.builder(DataForm.Type.form);
String instruction = "InstructionTest1"; String instruction = "InstructionTest1";
df.addInstruction(instruction); df.addInstruction(instruction);
FormField field = FormField.builder("testField1").build(); FormField field = FormField.builder("testField1").build();
df.addField(field); df.addField(field);
assertNotNull(df.toXML()); DataForm dataForm = df.build();
String output = df.toXML().toString(); String output = dataForm.toXML().toString();
assertEquals(TEST_OUTPUT_1, output); assertEquals(TEST_OUTPUT_1, output);
XmlPullParser parser = PacketParserUtils.getParserFor(output); XmlPullParser parser = PacketParserUtils.getParserFor(output);
df = pr.parse(parser); dataForm = pr.parse(parser);
assertNotNull(df); assertNotNull(dataForm);
assertNotNull(df.getFields()); assertNotNull(dataForm.getFields());
assertEquals(1 , df.getFields().size()); assertEquals(1 , dataForm.getFields().size());
assertEquals(1 , df.getInstructions().size()); assertEquals(1 , dataForm.getInstructions().size());
assertNotNull(df.toXML()); output = dataForm.toXML().toString();
output = df.toXML().toString();
assertEquals(TEST_OUTPUT_1, output); assertEquals(TEST_OUTPUT_1, output);
} }
@Test @Test
public void testLayout() throws Exception { public void testLayout() throws Exception {
// Build a Form. // Build a Form.
DataForm df = new DataForm(DataForm.Type.submit); DataForm.Builder df = DataForm.builder(DataForm.Type.form);
String instruction = "InstructionTest1"; String instruction = "InstructionTest1";
df.addInstruction(instruction); df.addInstruction(instruction);
FormField field = FormField.builder("testField1").build(); FormField field = FormField.builder("testField1").build();
@ -95,67 +94,62 @@ public class DataFormTest extends SmackTestSuite {
df.addExtensionElement(layout); df.addExtensionElement(layout);
DataForm dataForm = df.build();
assertNotNull(df.toXML()); String output = dataForm.toXML().toString();
String output = df.toXML().toString();
assertEquals(TEST_OUTPUT_2, output); assertEquals(TEST_OUTPUT_2, output);
XmlPullParser parser = PacketParserUtils.getParserFor(output); XmlPullParser parser = PacketParserUtils.getParserFor(output);
df = pr.parse(parser); dataForm = pr.parse(parser);
assertNotNull(df); assertNotNull(dataForm.getExtensionElements());
assertNotNull(df.getExtensionElements()); assertEquals(1 , dataForm.getExtensionElements().size());
assertEquals(1 , df.getExtensionElements().size()); Element element = dataForm.getExtensionElements().get(0);
Element element = df.getExtensionElements().get(0);
assertNotNull(element); assertNotNull(element);
layout = (DataLayout) element; layout = (DataLayout) element;
assertEquals(3 , layout.getPageLayout().size()); assertEquals(3 , layout.getPageLayout().size());
assertNotNull(df.toXML()); output = dataForm.toXML().toString();
output = df.toXML().toString();
assertEquals(TEST_OUTPUT_2, output); assertEquals(TEST_OUTPUT_2, output);
} }
@Test @Test
public void testValidation() throws Exception { public void testValidation() throws Exception {
// Build a Form. // Build a Form.
DataForm df = new DataForm(DataForm.Type.submit); DataForm.Builder df = DataForm.builder(DataForm.Type.form);
String instruction = "InstructionTest1"; String instruction = "InstructionTest1";
df.addInstruction(instruction); df.addInstruction(instruction);
FormField.Builder fieldBuilder = FormField.builder("testField1"); FormField.Builder<?, ?> fieldBuilder = FormField.builder("testField1");
ValidateElement dv = new RangeValidateElement("xs:integer", "1111", "9999"); ValidateElement dv = new RangeValidateElement("xs:integer", "1111", "9999");
fieldBuilder.addFormFieldChildElement(dv); fieldBuilder.addFormFieldChildElement(dv);
df.addField(fieldBuilder.build()); df.addField(fieldBuilder.build());
assertNotNull(df.toXML()); DataForm dataForm = df.build();
String output = df.toXML().toString(); String output = dataForm.toXML().toString();
assertEquals(TEST_OUTPUT_3, output); assertEquals(TEST_OUTPUT_3, output);
XmlPullParser parser = PacketParserUtils.getParserFor(output); XmlPullParser parser = PacketParserUtils.getParserFor(output);
df = pr.parse(parser); dataForm = pr.parse(parser);
assertNotNull(df); assertNotNull(dataForm.getFields());
assertNotNull(df.getFields()); assertEquals(1 , dataForm.getFields().size());
assertEquals(1 , df.getFields().size()); Element element = ValidateElement.from(dataForm.getFields().get(0));
Element element = ValidateElement.from(df.getFields().get(0));
assertNotNull(element); assertNotNull(element);
dv = (ValidateElement) element; dv = (ValidateElement) element;
assertEquals("xs:integer" , dv.getDatatype()); assertEquals("xs:integer" , dv.getDatatype());
assertNotNull(df.toXML()); output = dataForm.toXML().toString();
output = df.toXML().toString();
assertEquals(TEST_OUTPUT_3, output); assertEquals(TEST_OUTPUT_3, output);
} }
@Test @Test
public void testFixedField() throws Exception { public void testFixedField() throws Exception {
final String formWithFixedField = "<x xmlns='jabber:x:data' type='submit'><instructions>InstructionTest1</instructions><field type='fixed'></field></x>"; final String formWithFixedField = "<x xmlns='jabber:x:data' type='form'><instructions>InstructionTest1</instructions><field type='fixed'><value>Fixed field value</value></field></x>";
DataForm df = pr.parse(PacketParserUtils.getParserFor(formWithFixedField)); DataForm df = pr.parse(PacketParserUtils.getParserFor(formWithFixedField));
assertEquals(Type.fixed, df.getFields().get(0).getType()); assertEquals(Type.fixed, df.getFields().get(0).getType());
} }

View file

@ -20,6 +20,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import org.jivesoftware.smackx.xdata.FormField; import org.jivesoftware.smackx.xdata.FormField;
import org.jivesoftware.smackx.xdata.JidSingleFormField;
import org.jivesoftware.smackx.xdatavalidation.packet.ValidateElement.BasicValidateElement; import org.jivesoftware.smackx.xdatavalidation.packet.ValidateElement.BasicValidateElement;
import org.jivesoftware.smackx.xdatavalidation.packet.ValidateElement.ListRange; import org.jivesoftware.smackx.xdatavalidation.packet.ValidateElement.ListRange;
import org.jivesoftware.smackx.xdatavalidation.packet.ValidateElement.OpenValidateElement; import org.jivesoftware.smackx.xdatavalidation.packet.ValidateElement.OpenValidateElement;
@ -37,8 +38,7 @@ public class DataValidationHelperTest {
@Test @Test
public void testCheckConsistencyFormFieldBasicValidateElement() { public void testCheckConsistencyFormFieldBasicValidateElement() {
FormField.Builder field = FormField.builder("var") JidSingleFormField.Builder field = FormField.jidSingleBuilder("var");
.setType(FormField.Type.jid_single);
BasicValidateElement element = new BasicValidateElement(null); BasicValidateElement element = new BasicValidateElement(null);
ValidationConsistencyException vce = assertThrows(ValidationConsistencyException.class, ValidationConsistencyException vce = assertThrows(ValidationConsistencyException.class,
() -> element.checkConsistency(field)); () -> element.checkConsistency(field));
@ -52,15 +52,14 @@ public class DataValidationHelperTest {
vce = assertThrows(ValidationConsistencyException.class, () -> element.checkConsistency(field)); vce = assertThrows(ValidationConsistencyException.class, () -> element.checkConsistency(field));
assertEquals("Field type is not of type 'list-multi' while a 'list-range' is defined.", vce.getMessage()); assertEquals("Field type is not of type 'list-multi' while a 'list-range' is defined.", vce.getMessage());
FormField.Builder fieldListMulti = field.setType(FormField.Type.list_multi); FormField.Builder<?, ?> fieldListMulti = FormField.listMultiBuilder("var");
element.checkConsistency(fieldListMulti); element.checkConsistency(fieldListMulti);
} }
@Test @Test
public void testCheckConsistencyFormFieldOpenValidateElement() { public void testCheckConsistencyFormFieldOpenValidateElement() {
FormField.Builder field = FormField.builder("var") FormField.Builder<?, ?> field = FormField.hiddenBuilder("var");
.setType(FormField.Type.hidden);
OpenValidateElement element = new OpenValidateElement(null); OpenValidateElement element = new OpenValidateElement(null);
ValidationConsistencyException e = assertThrows(ValidationConsistencyException.class, ValidationConsistencyException e = assertThrows(ValidationConsistencyException.class,
() -> element.checkConsistency(field)); () -> element.checkConsistency(field));
@ -69,8 +68,7 @@ public class DataValidationHelperTest {
@Test @Test
public void testCheckConsistencyFormFieldRangeValidateElement() { public void testCheckConsistencyFormFieldRangeValidateElement() {
FormField.Builder field = FormField.builder("var") FormField.Builder<?, ?> field = FormField.textMultiBuilder("var");
.setType(FormField.Type.text_multi);
RangeValidateElement element = new RangeValidateElement("xs:integer", null, "99"); RangeValidateElement element = new RangeValidateElement("xs:integer", null, "99");
ValidationConsistencyException e = assertThrows(ValidationConsistencyException.class, ValidationConsistencyException e = assertThrows(ValidationConsistencyException.class,
() -> element.checkConsistency(field)); () -> element.checkConsistency(field));
@ -79,8 +77,7 @@ public class DataValidationHelperTest {
@Test @Test
public void testCheckConsistencyFormFieldRegexValidateElement() { public void testCheckConsistencyFormFieldRegexValidateElement() {
FormField.Builder field = FormField.builder("var") FormField.Builder<?, ?> field = FormField.listMultiBuilder("var");
.setType(FormField.Type.list_multi);
RegexValidateElement element = new RegexValidateElement(null, ".*"); RegexValidateElement element = new RegexValidateElement(null, ".*");
ValidationConsistencyException e = assertThrows(ValidationConsistencyException.class, ValidationConsistencyException e = assertThrows(ValidationConsistencyException.class,
() -> element.checkConsistency(field)); () -> element.checkConsistency(field));

View file

@ -27,6 +27,9 @@ import org.jivesoftware.smack.XMPPException.XMPPErrorException;
import org.jivesoftware.smack.packet.StandardExtensionElement; import org.jivesoftware.smack.packet.StandardExtensionElement;
import org.jivesoftware.smack.packet.StanzaError; import org.jivesoftware.smack.packet.StanzaError;
import org.jivesoftware.smackx.pubsub.form.ConfigureForm;
import org.jivesoftware.smackx.pubsub.form.FillableConfigureForm;
import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest; import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest;
import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment;
import org.igniterealtime.smack.inttest.TestNotPossibleException; import org.igniterealtime.smack.inttest.TestNotPossibleException;
@ -64,7 +67,7 @@ public class PubSubIntegrationTest extends AbstractSmackIntegrationTest {
public void transientNotificationOnlyNodeWithoutItemTest() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { public void transientNotificationOnlyNodeWithoutItemTest() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
final String nodename = "sinttest-transient-notificationonly-withoutitem-nodename-" + testRunId; final String nodename = "sinttest-transient-notificationonly-withoutitem-nodename-" + testRunId;
ConfigureForm defaultConfiguration = pubSubManagerOne.getDefaultConfiguration(); ConfigureForm defaultConfiguration = pubSubManagerOne.getDefaultConfiguration();
ConfigureForm config = new ConfigureForm(defaultConfiguration.createAnswerForm()); FillableConfigureForm config = defaultConfiguration.getFillableForm();
// Configure the node as "Notification-Only Node". // Configure the node as "Notification-Only Node".
config.setDeliverPayloads(false); config.setDeliverPayloads(false);
// Configure the node as "transient" (set persistent_items to 'false') // Configure the node as "transient" (set persistent_items to 'false')
@ -107,7 +110,7 @@ public class PubSubIntegrationTest extends AbstractSmackIntegrationTest {
final String itemId = "sinttest-transient-notificationonly-withitem-itemid-" + testRunId; final String itemId = "sinttest-transient-notificationonly-withitem-itemid-" + testRunId;
ConfigureForm defaultConfiguration = pubSubManagerOne.getDefaultConfiguration(); ConfigureForm defaultConfiguration = pubSubManagerOne.getDefaultConfiguration();
ConfigureForm config = new ConfigureForm(defaultConfiguration.createAnswerForm()); FillableConfigureForm config = defaultConfiguration.getFillableForm();
// Configure the node as "Notification-Only Node". // Configure the node as "Notification-Only Node".
config.setDeliverPayloads(false); config.setDeliverPayloads(false);
// Configure the node as "transient" (set persistent_items to 'false') // Configure the node as "transient" (set persistent_items to 'false')

View file

@ -26,7 +26,8 @@ import org.jivesoftware.smack.filter.ThreadFilter;
import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.StanzaBuilder; import org.jivesoftware.smack.packet.StanzaBuilder;
import org.jivesoftware.smackx.xdata.FormField.Type; import org.jivesoftware.smackx.xdata.form.FillableForm;
import org.jivesoftware.smackx.xdata.form.Form;
import org.jivesoftware.smackx.xdata.packet.DataForm; import org.jivesoftware.smackx.xdata.packet.DataForm;
import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest; import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest;
@ -55,40 +56,41 @@ public class FormTest extends AbstractSmackIntegrationTest {
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
@SmackIntegrationTest @SmackIntegrationTest
public void testFilloutForm() throws NotConnectedException, InterruptedException { public void testFilloutForm() throws NotConnectedException, InterruptedException {
Form formToSend = new Form(DataForm.Type.form); DataForm.Builder formToSend = DataForm.builder(DataForm.Type.form);
formToSend.setInstructions( formToSend.setInstructions(
"Fill out this form to report your case.\nThe case will be created automatically."); "Fill out this form to report your case.\nThe case will be created automatically.");
formToSend.setTitle("Case configurations"); formToSend.setTitle("Case configurations");
formToSend.setFormType("https://igniterealtime.org/projects/smack/sinttest/form-test/1");
// Add a hidden variable // Add a hidden variable
FormField.Builder field = FormField.builder("hidden_var"); FormField field = FormField.hiddenBuilder("hidden_var")
field.setType(FormField.Type.hidden); .setValue("Some value for the hidden variable")
field.addValue("Some value for the hidden variable"); .build();
formToSend.addField(field.build()); formToSend.addField(field);
// Add a fixed variable // Add a fixed variable
field = FormField.builder(); field = FormField.fixedBuilder()
field.addValue("Section 1: Case description"); .setValue("Section 1: Case description")
field.setType(Type.fixed); .build();
formToSend.addField(field.build()); formToSend.addField(field);
// Add a text-single variable // Add a text-single variable
field = FormField.builder("name"); field = FormField.textSingleBuilder("name")
field.setLabel("Enter a name for the case"); .setLabel("Enter a name for the case")
field.setType(FormField.Type.text_single); .build();
formToSend.addField(field.build()); formToSend.addField(field);
// Add a text-multi variable // Add a text-multi variable
field = FormField.builder("description"); field = FormField.textMultiBuilder("description")
field.setLabel("Enter a description"); .setLabel("Enter a description")
field.setType(FormField.Type.text_multi); .build();
formToSend.addField(field.build()); formToSend.addField(field);
// Add a boolean variable // Add a boolean variable
field = FormField.builder("time"); field = FormField.booleanBuilder("time")
field.setLabel("Is this your first case?"); .setLabel("Is this your first case?")
field.setType(FormField.Type.bool); .build();
formToSend.addField(field.build()); formToSend.addField(field);
// Add a text variable where an int value is expected // Add a text variable where an int value is expected
field = FormField.builder("age"); field = FormField.textSingleBuilder("age")
field.setLabel("How old are you?"); .setLabel("How old are you?")
field.setType(FormField.Type.text_single); .build();
formToSend.addField(field.build()); formToSend.addField(field);
// Create the chats between the two participants // Create the chats between the two participants
org.jivesoftware.smack.chat.Chat chat = org.jivesoftware.smack.chat.ChatManager.getInstanceFor(conOne).createChat(conTwo.getUser(), null); org.jivesoftware.smack.chat.Chat chat = org.jivesoftware.smack.chat.ChatManager.getInstanceFor(conOne).createChat(conTwo.getUser(), null);
@ -99,7 +101,7 @@ public class FormTest extends AbstractSmackIntegrationTest {
Message msg = StanzaBuilder.buildMessage() Message msg = StanzaBuilder.buildMessage()
.setBody("To enter a case please fill out this form and send it back to me") .setBody("To enter a case please fill out this form and send it back to me")
.addExtension(formToSend.getDataFormToSend()) .addExtension(formToSend.build())
.build(); .build();
try { try {
@ -110,12 +112,12 @@ public class FormTest extends AbstractSmackIntegrationTest {
Message msg2 = collector2.nextResult(); Message msg2 = collector2.nextResult();
assertNotNull(msg2, "Message not found"); assertNotNull(msg2, "Message not found");
// Retrieve the form to fill out // Retrieve the form to fill out
Form formToRespond = Form.getFormFrom(msg2); Form formToRespond = Form.from(msg2);
assertNotNull(formToRespond); assertNotNull(formToRespond);
assertNotNull(formToRespond.getField("name")); assertNotNull(formToRespond.getField("name"));
assertNotNull(formToRespond.getField("description")); assertNotNull(formToRespond.getField("description"));
// Obtain the form to send with the replies // Obtain the form to send with the replies
final Form completedForm = formToRespond.createAnswerForm(); final FillableForm completedForm = formToRespond.getFillableForm();
assertNotNull(completedForm.getField("hidden_var")); assertNotNull(completedForm.getField("hidden_var"));
// Check that a field of type String does not accept booleans // Check that a field of type String does not accept booleans
assertThrows(IllegalArgumentException.class, () -> completedForm.setAnswer("name", true)); assertThrows(IllegalArgumentException.class, () -> completedForm.setAnswer("name", true));
@ -132,7 +134,7 @@ public class FormTest extends AbstractSmackIntegrationTest {
.ofType(Message.Type.chat) .ofType(Message.Type.chat)
.setBody("To enter a case please fill out this form and send it back to me") .setBody("To enter a case please fill out this form and send it back to me")
// Add the completed form to the message // Add the completed form to the message
.addExtension(completedForm.getDataFormToSend()) .addExtension(completedForm.getDataFormToSubmit())
.build(); .build();
// Send the message with the completed form // Send the message with the completed form
conTwo.sendStanza(msg2); conTwo.sendStanza(msg2);
@ -141,7 +143,7 @@ public class FormTest extends AbstractSmackIntegrationTest {
Message msg3 = collector.nextResult(); Message msg3 = collector.nextResult();
assertNotNull(msg3, "Message not found"); assertNotNull(msg3, "Message not found");
// Retrieve the completed form // Retrieve the completed form
final Form completedForm2 = Form.getFormFrom(msg3); final DataForm completedForm2 = DataForm.from(msg3);
assertNotNull(completedForm2); assertNotNull(completedForm2);
assertNotNull(completedForm2.getField("name")); assertNotNull(completedForm2.getField("name"));
assertNotNull(completedForm2.getField("description")); assertNotNull(completedForm2.getField("description"));

View file

@ -75,7 +75,8 @@ import org.jivesoftware.smackx.workgroup.packet.Transcript;
import org.jivesoftware.smackx.workgroup.packet.Transcripts; import org.jivesoftware.smackx.workgroup.packet.Transcripts;
import org.jivesoftware.smackx.workgroup.settings.GenericSettings; import org.jivesoftware.smackx.workgroup.settings.GenericSettings;
import org.jivesoftware.smackx.workgroup.settings.SearchSettings; import org.jivesoftware.smackx.workgroup.settings.SearchSettings;
import org.jivesoftware.smackx.xdata.Form; import org.jivesoftware.smackx.xdata.form.FillableForm;
import org.jivesoftware.smackx.xdata.form.Form;
import org.jxmpp.jid.EntityBareJid; import org.jxmpp.jid.EntityBareJid;
import org.jxmpp.jid.EntityJid; import org.jxmpp.jid.EntityJid;
@ -573,7 +574,7 @@ public class AgentSession {
* @throws XMPPException if an XMPP protocol error was received. * @throws XMPPException if an XMPP protocol error was received.
* @throws InterruptedException if the calling thread was interrupted. * @throws InterruptedException if the calling thread was interrupted.
*/ */
public ReportedData searchTranscripts(Form completedForm) throws XMPPException, SmackException, InterruptedException { public ReportedData searchTranscripts(FillableForm completedForm) throws XMPPException, SmackException, InterruptedException {
return transcriptSearchManager.submitSearch(workgroupJID.asDomainBareJid(), return transcriptSearchManager.submitSearch(workgroupJID.asDomainBareJid(),
completedForm); completedForm);
} }

View file

@ -25,14 +25,15 @@ import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smackx.search.ReportedData; import org.jivesoftware.smackx.search.ReportedData;
import org.jivesoftware.smackx.workgroup.packet.TranscriptSearch; import org.jivesoftware.smackx.workgroup.packet.TranscriptSearch;
import org.jivesoftware.smackx.xdata.Form; import org.jivesoftware.smackx.xdata.form.FillableForm;
import org.jivesoftware.smackx.xdata.form.Form;
import org.jxmpp.jid.DomainBareJid; import org.jxmpp.jid.DomainBareJid;
/** /**
* A TranscriptSearchManager helps to retrieve the form to use for searching transcripts * A TranscriptSearchManager helps to retrieve the form to use for searching transcripts
* {@link #getSearchForm(DomainBareJid)} or to submit a search form and return the results of * {@link #getSearchForm(DomainBareJid)} or to submit a search form and return the results of
* the search {@link #submitSearch(DomainBareJid, Form)}. * the search {@link #submitSearch(DomainBareJid, FillableForm)}.
* *
* @author Gaston Dombiak * @author Gaston Dombiak
*/ */
@ -62,7 +63,7 @@ public class TranscriptSearchManager {
TranscriptSearch response = connection.createStanzaCollectorAndSend( TranscriptSearch response = connection.createStanzaCollectorAndSend(
search).nextResultOrThrow(); search).nextResultOrThrow();
return Form.getFormFrom(response); return Form.from(response);
} }
/** /**
@ -78,11 +79,11 @@ public class TranscriptSearchManager {
* @throws NotConnectedException if the XMPP connection is not connected. * @throws NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted. * @throws InterruptedException if the calling thread was interrupted.
*/ */
public ReportedData submitSearch(DomainBareJid serviceJID, Form completedForm) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { public ReportedData submitSearch(DomainBareJid serviceJID, FillableForm completedForm) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
TranscriptSearch search = new TranscriptSearch(); TranscriptSearch search = new TranscriptSearch();
search.setType(IQ.Type.get); search.setType(IQ.Type.get);
search.setTo(serviceJID); search.setTo(serviceJID);
search.addExtension(completedForm.getDataFormToSend()); search.addExtension(completedForm.getDataFormToSubmit());
TranscriptSearch response = connection.createStanzaCollectorAndSend( TranscriptSearch response = connection.createStanzaCollectorAndSend(
search).nextResultOrThrow(); search).nextResultOrThrow();

View file

@ -56,8 +56,10 @@ import org.jivesoftware.smackx.workgroup.settings.ChatSettings;
import org.jivesoftware.smackx.workgroup.settings.OfflineSettings; import org.jivesoftware.smackx.workgroup.settings.OfflineSettings;
import org.jivesoftware.smackx.workgroup.settings.SoundSettings; import org.jivesoftware.smackx.workgroup.settings.SoundSettings;
import org.jivesoftware.smackx.workgroup.settings.WorkgroupProperties; import org.jivesoftware.smackx.workgroup.settings.WorkgroupProperties;
import org.jivesoftware.smackx.xdata.Form;
import org.jivesoftware.smackx.xdata.FormField; import org.jivesoftware.smackx.xdata.FormField;
import org.jivesoftware.smackx.xdata.TextSingleFormField;
import org.jivesoftware.smackx.xdata.form.FillableForm;
import org.jivesoftware.smackx.xdata.form.Form;
import org.jivesoftware.smackx.xdata.packet.DataForm; import org.jivesoftware.smackx.xdata.packet.DataForm;
import org.jxmpp.jid.DomainBareJid; import org.jxmpp.jid.DomainBareJid;
@ -247,7 +249,7 @@ public class Workgroup {
* this method will throw an IllegalStateException if the user is already in the queue.<p> * this method will throw an IllegalStateException if the user is already in the queue.<p>
* *
* Some servers may be configured to require certain meta-data in order to * Some servers may be configured to require certain meta-data in order to
* join the queue. In that case, the {@link #joinQueue(Form)} method should be * join the queue. In that case, the {@link #joinQueue(FillableForm)} method should be
* used instead of this method so that meta-data may be passed in.<p> * used instead of this method so that meta-data may be passed in.<p>
* *
* The server tracks the conversations that a user has with agents over time. By * The server tracks the conversations that a user has with agents over time. By
@ -255,7 +257,7 @@ public class Workgroup {
* possible. For example, when the user is logged in anonymously using a web client. * possible. For example, when the user is logged in anonymously using a web client.
* In that case the user ID might be a randomly generated value put into a persistent * In that case the user ID might be a randomly generated value put into a persistent
* cookie or a username obtained via the session. A userID can be explicitly * cookie or a username obtained via the session. A userID can be explicitly
* passed in by using the {@link #joinQueue(Form, Jid)} method. When specified, * passed in by using the {@link #joinQueue(DataForm, Jid)} method. When specified,
* that userID will be used instead of the user's JID to track conversations. The * that userID will be used instead of the user's JID to track conversations. The
* server will ignore a manually specified userID if the user's connection to the server * server will ignore a manually specified userID if the user's connection to the server
* is not anonymous. * is not anonymous.
@ -293,7 +295,7 @@ public class Workgroup {
* possible. For example, when the user is logged in anonymously using a web client. * possible. For example, when the user is logged in anonymously using a web client.
* In that case the user ID might be a randomly generated value put into a persistent * In that case the user ID might be a randomly generated value put into a persistent
* cookie or a username obtained via the session. A userID can be explicitly * cookie or a username obtained via the session. A userID can be explicitly
* passed in by using the {@link #joinQueue(Form, Jid)} method. When specified, * passed in by using the {@link #joinQueue(DataForm, Jid)} method. When specified,
* that userID will be used instead of the user's JID to track conversations. The * that userID will be used instead of the user's JID to track conversations. The
* server will ignore a manually specified userID if the user's connection to the server * server will ignore a manually specified userID if the user's connection to the server
* is not anonymous. * is not anonymous.
@ -305,8 +307,8 @@ public class Workgroup {
* @throws SmackException if Smack detected an exceptional situation. * @throws SmackException if Smack detected an exceptional situation.
* @throws InterruptedException if the calling thread was interrupted. * @throws InterruptedException if the calling thread was interrupted.
*/ */
public void joinQueue(Form answerForm) throws XMPPException, SmackException, InterruptedException { public void joinQueue(FillableForm answerForm) throws XMPPException, SmackException, InterruptedException {
joinQueue(answerForm, null); joinQueue(answerForm.getDataFormToSubmit(), null);
} }
/** /**
@ -345,7 +347,7 @@ public class Workgroup {
* @throws NotConnectedException if the XMPP connection is not connected. * @throws NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted. * @throws InterruptedException if the calling thread was interrupted.
*/ */
public void joinQueue(Form answerForm, Jid userID) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { public void joinQueue(DataForm answerForm, Jid userID) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
// If already in the queue ignore the join request. // If already in the queue ignore the join request.
if (inQueue) { if (inQueue) {
throw new IllegalStateException("Already in queue " + workgroupJID); throw new IllegalStateException("Already in queue " + workgroupJID);
@ -400,18 +402,17 @@ public class Workgroup {
} }
// Build dataform from metadata // Build dataform from metadata
Form form = new Form(DataForm.Type.submit); DataForm.Builder form = DataForm.builder();
Iterator<String> iter = metadata.keySet().iterator(); Iterator<String> iter = metadata.keySet().iterator();
while (iter.hasNext()) { while (iter.hasNext()) {
String name = iter.next(); String name = iter.next();
String value = metadata.get(name).toString(); String value = metadata.get(name).toString();
FormField.Builder field = FormField.builder(name); TextSingleFormField.Builder field = FormField.builder(name);
field.setType(FormField.Type.text_single); field.setValue(value);
form.addField(field.build()); form.addField(field.build());
form.setAnswer(name, value);
} }
joinQueue(form, userID); joinQueue(form.build(), userID);
} }
/** /**
@ -569,14 +570,14 @@ public class Workgroup {
private final Jid userID; private final Jid userID;
private final DataForm form; private final DataForm form;
private JoinQueuePacket(EntityBareJid workgroup, Form answerForm, Jid userID) { private JoinQueuePacket(EntityBareJid workgroup, DataForm answerForm, Jid userID) {
super("join-queue", "http://jabber.org/protocol/workgroup"); super("join-queue", "http://jabber.org/protocol/workgroup");
this.userID = userID; this.userID = userID;
setTo(workgroup); setTo(workgroup);
setType(IQ.Type.set); setType(IQ.Type.set);
form = answerForm.getDataFormToSend(); form = answerForm;
addExtension(form); addExtension(form);
} }
@ -772,6 +773,6 @@ public class Workgroup {
WorkgroupForm response = connection.createStanzaCollectorAndSend( WorkgroupForm response = connection.createStanzaCollectorAndSend(
workgroupForm).nextResultOrThrow(); workgroupForm).nextResultOrThrow();
return Form.getFormFrom(response); return Form.from(response);
} }
} }

View file

@ -37,14 +37,14 @@ import org.jivesoftware.smackx.ox.element.PublicKeysListElement;
import org.jivesoftware.smackx.ox.element.SecretkeyElement; import org.jivesoftware.smackx.ox.element.SecretkeyElement;
import org.jivesoftware.smackx.pep.PepManager; import org.jivesoftware.smackx.pep.PepManager;
import org.jivesoftware.smackx.pubsub.AccessModel; import org.jivesoftware.smackx.pubsub.AccessModel;
import org.jivesoftware.smackx.pubsub.ConfigureForm;
import org.jivesoftware.smackx.pubsub.Item; import org.jivesoftware.smackx.pubsub.Item;
import org.jivesoftware.smackx.pubsub.LeafNode; import org.jivesoftware.smackx.pubsub.LeafNode;
import org.jivesoftware.smackx.pubsub.Node; import org.jivesoftware.smackx.pubsub.Node;
import org.jivesoftware.smackx.pubsub.PayloadItem; import org.jivesoftware.smackx.pubsub.PayloadItem;
import org.jivesoftware.smackx.pubsub.PubSubException; import org.jivesoftware.smackx.pubsub.PubSubException;
import org.jivesoftware.smackx.pubsub.PubSubManager; import org.jivesoftware.smackx.pubsub.PubSubManager;
import org.jivesoftware.smackx.xdata.packet.DataForm; import org.jivesoftware.smackx.pubsub.form.ConfigureForm;
import org.jivesoftware.smackx.pubsub.form.FillableConfigureForm;
import org.jxmpp.jid.BareJid; import org.jxmpp.jid.BareJid;
import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.OpenPgpV4Fingerprint;
@ -101,7 +101,7 @@ public class OpenPgpPubSubUtil {
SmackException.NoResponseException { SmackException.NoResponseException {
ConfigureForm current = node.getNodeConfiguration(); ConfigureForm current = node.getNodeConfiguration();
if (current.getAccessModel() != accessModel) { if (current.getAccessModel() != accessModel) {
ConfigureForm updateConfig = new ConfigureForm(DataForm.Type.submit); FillableConfigureForm updateConfig = current.getFillableForm();
updateConfig.setAccessModel(accessModel); updateConfig.setAccessModel(accessModel);
node.sendConfigurationForm(updateConfig); node.sendConfigurationForm(updateConfig);
} }