diff --git a/.travis.yml b/.travis.yml index ad03d90f1..10f7b90f5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,8 +4,7 @@ android: components: - android-19 jdk: - - oraclejdk8 - - openjdk9 + - openjdk8 - openjdk11 before_cache: diff --git a/README.md b/README.md index 7ae8508f4..1e22423ce 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Smack ===== -[![Build Status](https://travis-ci.org/igniterealtime/Smack.svg?branch=master)](https://travis-ci.org/igniterealtime/Smack) [![Coverage Status](https://coveralls.io/repos/igniterealtime/Smack/badge.svg)](https://coveralls.io/r/igniterealtime/Smack) [![Project Stats](https://www.openhub.net/p/smack/widgets/project_thin_badge.gif)](https://www.openhub.net/p/smack) [![Link to XMPP chat smack@conference.igniterealtime.org](https://inverse.chat/badge.svg?room=smack@conference.igniterealtime.org)](https://inverse.chat/#converse/room?jid=smack@conference.igniterealtime.org) +[![Build Status](https://api.travis-ci.com/igniterealtime/Smack.svg?branch=master)](https://travis-ci.com/github/igniterealtime/Smack) [![Coverage Status](https://coveralls.io/repos/igniterealtime/Smack/badge.svg)](https://coveralls.io/r/igniterealtime/Smack) [![Project Stats](https://www.openhub.net/p/smack/widgets/project_thin_badge.gif)](https://www.openhub.net/p/smack) [![Link to XMPP chat smack@conference.igniterealtime.org](https://inverse.chat/badge.svg?room=smack@conference.igniterealtime.org)](https://inverse.chat/#converse/room?jid=smack@conference.igniterealtime.org) About ----- diff --git a/build.gradle b/build.gradle index b0a81ba9f..76cf80d61 100644 --- a/build.gradle +++ b/build.gradle @@ -101,20 +101,6 @@ allprojects { ':smack-omemo-signal-integration-test', ':smack-repl' ].collect{ project(it) } - // When this list is empty, then move the according javadoc - // tool Werror option into the global configure section. - nonStrictJavadocProjects = [ - ':smack-bosh', - ':smack-core', - ':smack-experimental', - ':smack-extensions', - ':smack-im', - ':smack-integration-test', - ':smack-jingle-old', - ':smack-legacy', - ':smack-omemo', - ':smack-tcp', - ].collect{ project(it) } // Lazily evaluate the Android bootClasspath and offline // Javadoc using a closure, so that targets which do not // require it are still able to succeed without an Android @@ -136,9 +122,11 @@ allprojects { // Default to true useSonatype = true } + javaCompatilibity = JavaVersion.VERSION_1_8 + javaMajor = javaCompatilibity.getMajorVersion() } group = 'org.igniterealtime.smack' - sourceCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = javaCompatilibity targetCompatibility = sourceCompatibility version = shortVersion if (isSnapshot) { @@ -258,7 +246,19 @@ allprojects { options.addStringOption('Xwerror', '-quiet') } } - tasks.withType(Javadoc) { + + if (JavaVersion.current().isJava9Compatible()) { + tasks.withType(Javadoc) { + options.addStringOption('-release', javaMajor) + } + tasks.withType(JavaCompile) { + options.compilerArgs.addAll([ + '--release', javaMajor, + ]) + } + } + +tasks.withType(Javadoc) { options.charSet = "UTF-8" options.encoding = 'UTF-8' } @@ -304,16 +304,10 @@ task javadocAll(type: Javadoc) { project.sourceSets.main.compileClasspath}) classpath += files(androidBootClasspath) options { - // Add source compatiblitiy statement to work around bug in JDK 11 - // See - // - https://bugs.openjdk.java.net/browse/JDK-8217177 - // - http://hg.openjdk.java.net/jdk/jdk/rev/8ce4083fc831 - // - https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=920020 - source = sourceCompatibility linkSource = true use = true links = [ - "https://docs.oracle.com/javase/${sourceCompatibility.getMajorVersion()}/docs/api/", + "https://docs.oracle.com/javase/${javaMajor}/docs/api/", "https://jxmpp.org/releases/$jxmppVersion/javadoc/", "https://minidns.org/releases/$miniDnsVersion/javadoc/", ] as String[] @@ -571,15 +565,6 @@ subprojects*.jar { } } -configure(subprojects - nonStrictJavadocProjects) { - tasks.withType(Javadoc) { - // Abort on javadoc warnings. - // See JDK-8200363 (https://bugs.openjdk.java.net/browse/JDK-8200363) - // for information about the -Xwerror option. - options.addStringOption('Xwerror', '-quiet') - } -} - configure(subprojects - gplLicensedProjects) { checkstyle { configProperties.checkstyleLicenseHeader = "header" diff --git a/documentation/developer/integrationtest.md b/documentation/developer/integrationtest.md index 16119c6f1..fe006b58c 100644 --- a/documentation/developer/integrationtest.md +++ b/documentation/developer/integrationtest.md @@ -102,7 +102,7 @@ The base class that integration tests need to subclass. ### `AbstractSmackLowLevelIntegrationTest` -Allows low level integration test, i.e. ever test method will have its on exclusive XMPPTCPConnection instances. +Allows low level integration test, i.e. every test method will have its own exclusive XMPPTCPConnection instances. ### `AbstractSmackSpecificLowLevelIntegrationTest` diff --git a/documentation/extensions/index.md b/documentation/extensions/index.md index 47100f37f..91f291ca4 100644 --- a/documentation/extensions/index.md +++ b/documentation/extensions/index.md @@ -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 | | [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. | +| 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. | | 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. | @@ -120,6 +121,7 @@ Experimental Smack Extensions and currently supported XEPs of smack-experimental | [Consistent Color Generation](consistent_colors.md) | [XEP-0392](https://xmpp.org/extensions/xep-0392.html) | 0.6.0 | Generate consistent colors for identifiers like usernames to provide a consistent user experience. | | [Message Markup](messagemarkup.md) | [XEP-0394](https://xmpp.org/extensions/xep-0394.html) | 0.1.0 | Style message bodies while keeping body and markup information separated. | | DNS Queries over XMPP (DoX) | [XEP-0418](https://xmpp.org/extensions/xep-0418.html) | 0.1.0 | Send DNS queries and responses over XMPP. | +| Message Fastening | [XEP-0422](https://xmpp.org/extensions/xep-0422.html) | 0.1.1 | Mark payloads on a message to be logistically fastened to a previous message. | Unofficial XMPP Extensions -------------------------- diff --git a/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java b/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java index 66ebb1916..87f6f091e 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java @@ -527,7 +527,8 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { saslFeatureReceived.init(); lastFeaturesReceived.init(); tlsHandled.init(); - closingStreamReceived.init(); + // TODO: We do not init() closingStreamReceived here, as the integration tests use it to check if we waited for + // it. } /** @@ -549,6 +550,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { // Reset the connection state initState(); + closingStreamReceived.init(); streamId = null; try { diff --git a/smack-core/src/main/java/org/jivesoftware/smack/filter/ExtensionElementFilter.java b/smack-core/src/main/java/org/jivesoftware/smack/filter/ExtensionElementFilter.java new file mode 100644 index 000000000..ff84f403d --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/filter/ExtensionElementFilter.java @@ -0,0 +1,53 @@ +/** + * + * 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.smack.filter; + +import javax.xml.namespace.QName; + +import org.jivesoftware.smack.packet.ExtensionElement; +import org.jivesoftware.smack.packet.Stanza; +import org.jivesoftware.smack.util.XmppElementUtil; + +public class ExtensionElementFilter implements StanzaFilter { + + private final Class extensionElementClass; + private final QName extensionElementQName; + + protected ExtensionElementFilter(Class extensionElementClass) { + this.extensionElementClass = extensionElementClass; + extensionElementQName = XmppElementUtil.getQNameFor(extensionElementClass); + } + + @Override + public final boolean accept(Stanza stanza) { + ExtensionElement extensionElement = stanza.getExtension(extensionElementQName); + if (extensionElement == null) { + return false; + } + + if (!extensionElementClass.isInstance(extensionElement)) { + return false; + } + + E specificExtensionElement = extensionElementClass.cast(extensionElement); + return accept(specificExtensionElement); + } + + public boolean accept(E extensionElement) { + return true; + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/filter/MessageTypeFilter.java b/smack-core/src/main/java/org/jivesoftware/smack/filter/MessageTypeFilter.java index 61d6ed276..6d70dbae3 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/filter/MessageTypeFilter.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/filter/MessageTypeFilter.java @@ -35,6 +35,7 @@ public final class MessageTypeFilter extends FlexibleStanzaTypeFilter { public static final StanzaFilter HEADLINE = new MessageTypeFilter(Type.headline); public static final StanzaFilter ERROR = new MessageTypeFilter(Type.error); public static final StanzaFilter NORMAL_OR_CHAT = new OrFilter(NORMAL, CHAT); + public static final StanzaFilter NORMAL_OR_HEADLINE = new OrFilter(NORMAL, HEADLINE); public static final StanzaFilter NORMAL_OR_CHAT_OR_HEADLINE = new OrFilter(NORMAL_OR_CHAT, HEADLINE); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/filter/PacketExtensionFilter.java b/smack-core/src/main/java/org/jivesoftware/smack/filter/PacketExtensionFilter.java deleted file mode 100644 index 8b0f40cd5..000000000 --- a/smack-core/src/main/java/org/jivesoftware/smack/filter/PacketExtensionFilter.java +++ /dev/null @@ -1,79 +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.smack.filter; - -import org.jivesoftware.smack.packet.ExtensionElement; -import org.jivesoftware.smack.packet.Stanza; -import org.jivesoftware.smack.util.StringUtils; - -/** - * Filters for packets with a particular type of stanza extension. - * - * @author Matt Tucker - * @deprecated use {@link StanzaExtensionFilter} instead. - */ -@Deprecated -public class PacketExtensionFilter implements StanzaFilter { - - private final String elementName; - private final String namespace; - - /** - * Creates a new stanza extension filter. Packets will pass the filter if - * they have a stanza extension that matches the specified element name - * and namespace. - * - * @param elementName the XML element name of the stanza extension. - * @param namespace the XML namespace of the stanza extension. - */ - public PacketExtensionFilter(String elementName, String namespace) { - StringUtils.requireNotNullNorEmpty(namespace, "namespace must not be null nor empty"); - - this.elementName = elementName; - this.namespace = namespace; - } - - /** - * Creates a new stanza extension filter. Packets will pass the filter if they have a packet - * extension that matches the specified namespace. - * - * @param namespace the XML namespace of the stanza extension. - */ - public PacketExtensionFilter(String namespace) { - this(null, namespace); - } - - /** - * Creates a new stanza extension filter for the given stanza extension. - * - * @param packetExtension TODO javadoc me please - */ - public PacketExtensionFilter(ExtensionElement packetExtension) { - this(packetExtension.getElementName(), packetExtension.getNamespace()); - } - - @Override - public boolean accept(Stanza packet) { - return packet.hasExtension(elementName, namespace); - } - - @Override - public String toString() { - return getClass().getSimpleName() + ": element=" + elementName + " namespace=" + namespace; - } -} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/filter/PacketFilter.java b/smack-core/src/main/java/org/jivesoftware/smack/filter/PacketFilter.java deleted file mode 100644 index 10f9b834f..000000000 --- a/smack-core/src/main/java/org/jivesoftware/smack/filter/PacketFilter.java +++ /dev/null @@ -1,51 +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.smack.filter; - -/** - * Defines a way to filter packets for particular attributes. Stanza filters are used when - * constructing stanza listeners or collectors -- the filter defines what packets match the criteria - * of the collector or listener for further stanza processing. - *

- * Several simple filters are pre-defined. These filters can be logically combined for more complex - * stanza filtering by using the {@link org.jivesoftware.smack.filter.AndFilter AndFilter} and - * {@link org.jivesoftware.smack.filter.OrFilter OrFilter} filters. It's also possible to define - * your own filters by implementing this interface. The code example below creates a trivial filter - * for packets with a specific ID (real code should use {@link StanzaIdFilter} instead). - * - *

- * // Use an anonymous inner class to define a stanza filter that returns
- * // all packets that have a stanza ID of "RS145".
- * PacketFilter myFilter = new PacketFilter() {
- *     public boolean accept(Packet packet) {
- *         return "RS145".equals(packet.getStanzaId());
- *     }
- * };
- * // Create a new stanza collector using the filter we created.
- * StanzaCollector myCollector = packetReader.createStanzaCollector(myFilter);
- * 
- * - * @see org.jivesoftware.smack.StanzaCollector - * @see org.jivesoftware.smack.StanzaListener - * @author Matt Tucker - * @deprecated use {@link StanzaFilter} - */ -@Deprecated -public interface PacketFilter extends StanzaFilter { - -} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/filter/PacketIDFilter.java b/smack-core/src/main/java/org/jivesoftware/smack/filter/PacketIDFilter.java deleted file mode 100644 index 643f3f3fe..000000000 --- a/smack-core/src/main/java/org/jivesoftware/smack/filter/PacketIDFilter.java +++ /dev/null @@ -1,66 +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.smack.filter; - -import org.jivesoftware.smack.packet.Stanza; -import org.jivesoftware.smack.util.StringUtils; - -/** - * Filters for packets with a particular stanza ID. - * - * @author Matt Tucker - * @deprecated use {@link StanzaIdFilter} instead. - */ -@Deprecated -public class PacketIDFilter implements StanzaFilter { - - private final String packetID; - - /** - * Creates a new stanza ID filter using the specified packet's ID. - * - * @param packet the stanza which the ID is taken from. - * @deprecated use {@link StanzaIdFilter#StanzaIdFilter(Stanza)} instead. - */ - @Deprecated - public PacketIDFilter(Stanza packet) { - this(packet.getStanzaId()); - } - - /** - * Creates a new stanza ID filter using the specified stanza ID. - * - * @param packetID the stanza ID to filter for. - * @deprecated use {@link StanzaIdFilter#StanzaIdFilter(Stanza)} instead. - */ - @Deprecated - public PacketIDFilter(String packetID) { - StringUtils.requireNotNullNorEmpty(packetID, "Packet ID must not be null nor empty."); - this.packetID = packetID; - } - - @Override - public boolean accept(Stanza packet) { - return packetID.equals(packet.getStanzaId()); - } - - @Override - public String toString() { - return getClass().getSimpleName() + ": id=" + packetID; - } -} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/filter/PacketTypeFilter.java b/smack-core/src/main/java/org/jivesoftware/smack/filter/PacketTypeFilter.java deleted file mode 100644 index 37d37a49c..000000000 --- a/smack-core/src/main/java/org/jivesoftware/smack/filter/PacketTypeFilter.java +++ /dev/null @@ -1,63 +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.smack.filter; - -import org.jivesoftware.smack.packet.Message; -import org.jivesoftware.smack.packet.Presence; -import org.jivesoftware.smack.packet.Stanza; - -/** - * Filters for packets of a particular type. The type is given as a Class object, so - * example types would: - *
    - *
  • Message.class - *
  • IQ.class - *
  • Presence.class - *
- * - * @author Matt Tucker - * @deprecated use {@link StanzaTypeFilter} instead. - */ -@Deprecated -public class PacketTypeFilter implements StanzaFilter { - - public static final PacketTypeFilter PRESENCE = new PacketTypeFilter(Presence.class); - public static final PacketTypeFilter MESSAGE = new PacketTypeFilter(Message.class); - - private final Class packetType; - - /** - * Creates a new stanza type filter that will filter for packets that are the - * same type as packetType. - * - * @param packetType the Class type. - */ - public PacketTypeFilter(Class packetType) { - this.packetType = packetType; - } - - @Override - public boolean accept(Stanza packet) { - return packetType.isInstance(packet); - } - - @Override - public String toString() { - return getClass().getSimpleName() + ": " + packetType.getName(); - } -} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/filter/ToFilter.java b/smack-core/src/main/java/org/jivesoftware/smack/filter/ToFilter.java deleted file mode 100644 index 94da6a332..000000000 --- a/smack-core/src/main/java/org/jivesoftware/smack/filter/ToFilter.java +++ /dev/null @@ -1,50 +0,0 @@ -/** - * - * Copyright © 2014 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.smack.filter; - -import org.jivesoftware.smack.packet.Stanza; - -import org.jxmpp.jid.Jid; - -/** - * Match based on the 'to' attribute of a Stanza. - * - * @deprecated use {@link ToMatchesFilter} instead. - */ -@Deprecated -public class ToFilter implements StanzaFilter { - - private final Jid to; - - public ToFilter(Jid to) { - this.to = to; - } - - @Override - public boolean accept(Stanza packet) { - Jid packetTo = packet.getTo(); - if (packetTo == null) { - return false; - } - return packetTo.equals(to); - } - - @Override - public String toString() { - return getClass().getSimpleName() + ": to=" + to; - } -} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/filter/jidtype/FromJidTypeFilter.java b/smack-core/src/main/java/org/jivesoftware/smack/filter/jidtype/FromJidTypeFilter.java index c9326c175..3bcd71b22 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/filter/jidtype/FromJidTypeFilter.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/filter/jidtype/FromJidTypeFilter.java @@ -28,6 +28,8 @@ import org.jxmpp.jid.Jid; */ public class FromJidTypeFilter extends AbstractJidTypeFilter { + public static final FromJidTypeFilter ENTITY_BARE_JID = new FromJidTypeFilter(JidType.EntityBareJid); + public FromJidTypeFilter(JidType jidType) { super(jidType); } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/Stanza.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/Stanza.java index bedcad481..15c82127e 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/Stanza.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/Stanza.java @@ -445,7 +445,7 @@ public abstract class Stanza implements StanzaView, TopLevelStreamElement { * @param extensions a collection of stanza extensions */ // TODO: Mark this as deprecated once StanzaBuilder is ready and all call sites are gone. - public final void addExtensions(Collection extensions) { + public final void addExtensions(Collection extensions) { if (extensions == null) return; for (ExtensionElement packetExtension : extensions) { addExtension(packetExtension); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/StanzaView.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/StanzaView.java index 9ee3b6933..7745ecc2a 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/StanzaView.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/StanzaView.java @@ -106,5 +106,12 @@ public interface StanzaView extends XmlLangElement { List getExtensions(QName qname); + /** + * Return all extension elements of the given type. Returns the empty list if there a none. + * + * @param the type of extension elements. + * @param extensionElementClass the class of the type of extension elements. + * @return a list of extension elements of that type, which may be empty. + */ List getExtensions(Class extensionElementClass); } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/parsing/SmackParsingException.java b/smack-core/src/main/java/org/jivesoftware/smack/parsing/SmackParsingException.java index 82502736f..08b4586fb 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/parsing/SmackParsingException.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/parsing/SmackParsingException.java @@ -1,6 +1,6 @@ /** * - * Copyright 2019 Florian Schmaus + * Copyright 2019-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. @@ -30,6 +30,10 @@ public class SmackParsingException extends Exception { super(exception); } + public SmackParsingException(String message) { + super(message); + } + public static class SmackTextParseException extends SmackParsingException { /** * diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/CollectionUtil.java b/smack-core/src/main/java/org/jivesoftware/smack/util/CollectionUtil.java index eb76ef3be..b35dfb4a0 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/CollectionUtil.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/CollectionUtil.java @@ -18,9 +18,12 @@ package org.jivesoftware.smack.util; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Set; public class CollectionUtil { @@ -59,6 +62,20 @@ public class CollectionUtil { return new ArrayList<>(collection); } + public static List cloneAndSeal(Collection collection) { + if (collection == null) { + return Collections.emptyList(); + } + + ArrayList clone = newListWith(collection); + return Collections.unmodifiableList(clone); + } + + public static Map cloneAndSeal(Map map) { + Map clone = new HashMap<>(map); + return Collections.unmodifiableMap(clone); + } + public static Set newSetWith(Collection collection) { if (collection == null) { return null; diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/EqualsUtil.java b/smack-core/src/main/java/org/jivesoftware/smack/util/EqualsUtil.java index 9d786cc8f..8110358a7 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/EqualsUtil.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/EqualsUtil.java @@ -1,6 +1,6 @@ /** * - * Copyright 2019 Florian Schmaus. + * Copyright 2019-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. @@ -34,6 +34,12 @@ public final class EqualsUtil { return false; } + int thisHashCode = thisObject.hashCode(); + int otherHashCode = other.hashCode(); + if (thisHashCode != otherHashCode) { + return false; + } + EqualsUtil.Builder equalsBuilder = new EqualsUtil.Builder(); equalsComperator.compare(equalsBuilder, thisObjectClass.cast(other)); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/PacketUtil.java b/smack-core/src/main/java/org/jivesoftware/smack/util/PacketUtil.java index 6124e66f4..59755660c 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/PacketUtil.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/PacketUtil.java @@ -1,6 +1,6 @@ /** * - * Copyright © 2014 Florian Schmaus + * Copyright © 2014-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. @@ -22,40 +22,6 @@ import org.jivesoftware.smack.packet.ExtensionElement; public class PacketUtil { - /** - * Get a extension element from a collection. - * @param collection TODO javadoc me please - * @param element TODO javadoc me please - * @param namespace TODO javadoc me please - * @param the type of the extension element. - * @return the extension element - * @deprecated use {@link #extensionElementFrom(Collection, String, String)} instead. - */ - @Deprecated - public static PE packetExtensionfromCollection( - Collection collection, String element, - String namespace) { - return extensionElementFrom(collection, element, namespace); - } - - /** - * Get a extension element from a collection. - * - * @param collection Collection of ExtensionElements. - * @param element name of the targeted ExtensionElement. - * @param namespace namespace of the targeted ExtensionElement. - * @param Type of the ExtensionElement - * - * @return the extension element - * @deprecated use {@link #extensionElementFrom(Collection, String, String)} instead - */ - @Deprecated - public static PE packetExtensionFromCollection( - Collection collection, String element, - String namespace) { - return extensionElementFrom(collection, element, namespace); - } - /** * Get a extension element from a collection. * diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/StringUtils.java b/smack-core/src/main/java/org/jivesoftware/smack/util/StringUtils.java index b2958395b..c966b2d34 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/StringUtils.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/StringUtils.java @@ -20,8 +20,10 @@ package org.jivesoftware.smack.util; import java.io.IOException; import java.nio.CharBuffer; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.Collection; import java.util.Iterator; +import java.util.List; import java.util.Random; import java.util.regex.Pattern; @@ -591,4 +593,11 @@ public class StringUtils { } return appendable.append('\n'); } + + public static final String PORTABLE_NEWLINE_REGEX = "\\r?\\n"; + + public static List splitLinesPortable(String input) { + String[] lines = input.split(PORTABLE_NEWLINE_REGEX); + return Arrays.asList(lines); + } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/XmlStringBuilder.java b/smack-core/src/main/java/org/jivesoftware/smack/util/XmlStringBuilder.java index 145b1d3ec..c483db913 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/XmlStringBuilder.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/XmlStringBuilder.java @@ -77,6 +77,10 @@ public class XmlStringBuilder implements Appendable, CharSequence, Element { .build(); } + public XmlEnvironment getXmlEnvironment() { + return effectiveXmlEnvironment; + } + public XmlStringBuilder escapedElement(String name, String escapedContent) { assert escapedContent != null; openElement(name); @@ -493,6 +497,13 @@ public class XmlStringBuilder implements Appendable, CharSequence, Element { return this; } + public XmlStringBuilder optAppend(Collection elements) { + if (elements != null) { + append(elements); + } + return this; + } + public XmlStringBuilder optTextChild(CharSequence sqc, NamedElement parentElement) { if (sqc == null) { return closeEmptyElement(); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/XmppElementUtil.java b/smack-core/src/main/java/org/jivesoftware/smack/util/XmppElementUtil.java index 336e1e21e..34ccb0d81 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/XmppElementUtil.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/XmppElementUtil.java @@ -49,7 +49,7 @@ public class XmppElementUtil { namespace = (String) fullyQualifiedElement.getField("NAMESPACE").get(null); } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | SecurityException e) { - throw new IllegalArgumentException("The class" + fullyQualifiedElement + " has no ELEMENT, NAMSEPACE or QNAME member. Consider adding QNAME", e); + throw new IllegalArgumentException("The class" + fullyQualifiedElement + " has no ELEMENT, NAMESPACE or QNAME member. Consider adding QNAME", e); } return new QName(namespace, element); diff --git a/smack-debug/src/main/java/org/jivesoftware/smackx/debugger/EnhancedDebuggerWindow.java b/smack-debug/src/main/java/org/jivesoftware/smackx/debugger/EnhancedDebuggerWindow.java index 513e28663..a41c9ab78 100644 --- a/smack-debug/src/main/java/org/jivesoftware/smackx/debugger/EnhancedDebuggerWindow.java +++ b/smack-debug/src/main/java/org/jivesoftware/smackx/debugger/EnhancedDebuggerWindow.java @@ -143,6 +143,7 @@ public final class EnhancedDebuggerWindow { debugger.tabbedPane.setName("XMPPConnection_" + tabbedPane.getComponentCount()); tabbedPane.add(debugger.tabbedPane, tabbedPane.getComponentCount() - 1); tabbedPane.setIconAt(tabbedPane.indexOfComponent(debugger.tabbedPane), connectionCreatedIcon); + tabbedPane.setSelectedIndex(tabbedPane.indexOfComponent(debugger.tabbedPane)); frame.setTitle( "Smack Debug Window -- Total connections: " + (tabbedPane.getComponentCount() - 1)); // Keep the added debugger for later access diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/MamManager.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/MamManager.java index 9bbbe301e..3398df23c 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/MamManager.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/MamManager.java @@ -278,8 +278,9 @@ public final class MamManager extends Manager { if (dataForm != null) { return dataForm; } - dataForm = getNewMamForm(); - dataForm.addFields(formFields.values()); + DataForm.Builder dataFormBuilder = getNewMamForm(); + dataFormBuilder.addFields(formFields.values()); + dataForm = dataFormBuilder.build(); return dataForm; } @@ -330,7 +331,7 @@ public final class MamManager extends Manager { } FormField formField = getWithFormField(withJid); - formFields.put(formField.getVariable(), formField); + formFields.put(formField.getFieldName(), formField); return this; } @@ -341,9 +342,9 @@ public final class MamManager extends Manager { } FormField formField = FormField.builder(FORM_FIELD_START) - .addValue(start) + .setValue(start) .build(); - formFields.put(formField.getVariable(), formField); + formFields.put(formField.getFieldName(), formField); FormField endFormField = formFields.get(FORM_FIELD_END); if (endFormField != null) { @@ -369,9 +370,9 @@ public final class MamManager extends Manager { } FormField formField = FormField.builder(FORM_FIELD_END) - .addValue(end) + .setValue(end) .build(); - formFields.put(formField.getVariable(), formField); + formFields.put(formField.getFieldName(), formField); FormField startFormField = formFields.get(FORM_FIELD_START); if (startFormField != null) { @@ -418,7 +419,7 @@ public final class MamManager extends Manager { } public Builder withAdditionalFormField(FormField formField) { - formFields.put(formField.getVariable(), formField); + formFields.put(formField.getFieldName(), formField); return this; } @@ -483,7 +484,7 @@ public final class MamManager extends Manager { private static FormField getWithFormField(Jid withJid) { return FormField.builder(FORM_FIELD_WITH) - .addValue(withJid.toString()) + .setValue(withJid.toString()) .build(); } @@ -718,9 +719,9 @@ public final class MamManager extends Manager { throw new SmackException.FeatureNotSupportedException(ADVANCED_CONFIG_NODE, archiveAddress); } - private static DataForm getNewMamForm() { - FormField field = FormField.hiddenFormType(MamElements.NAMESPACE); - DataForm form = new DataForm(DataForm.Type.submit); + private static DataForm.Builder getNewMamForm() { + FormField field = FormField.buildHiddenFormType(MamElements.NAMESPACE); + DataForm.Builder form = DataForm.builder(); form.addField(field); return form; } diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/element/MamQueryIQ.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/element/MamQueryIQ.java index 787de8a37..b41400505 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/element/MamQueryIQ.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/element/MamQueryIQ.java @@ -1,6 +1,6 @@ /** * - * Copyright © 2016 Florian Schmaus and Fernando Ramirez + * Copyright © 2016-2020 Florian Schmaus and Fernando Ramirez * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,6 @@ package org.jivesoftware.smackx.mam.element; import org.jivesoftware.smack.packet.IQ; -import org.jivesoftware.smackx.xdata.FormField; import org.jivesoftware.smackx.xdata.packet.DataForm; /** @@ -88,11 +87,11 @@ public class MamQueryIQ extends IQ { this.dataForm = dataForm; if (dataForm != null) { - FormField field = dataForm.getHiddenFormTypeField(); - if (field == null) { + String formType = dataForm.getFormType(); + if (formType == null) { throw new IllegalArgumentException("If a data form is given it must posses a hidden form type field"); } - if (!field.getValues().get(0).toString().equals(MamElements.NAMESPACE)) { + if (!formType.equals(MamElements.NAMESPACE)) { throw new IllegalArgumentException( "Value of the hidden form type field must be '" + MamElements.NAMESPACE + "'"); } diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/MessageFasteningManager.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/MessageFasteningManager.java new file mode 100644 index 000000000..d66cd1bff --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/MessageFasteningManager.java @@ -0,0 +1,107 @@ +/** + * + * Copyright 2019 Paul Schaub + * + * 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.message_fastening; + +import java.util.List; +import java.util.WeakHashMap; + +import org.jivesoftware.smack.ConnectionCreationListener; +import org.jivesoftware.smack.Manager; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPConnectionRegistry; +import org.jivesoftware.smack.packet.MessageBuilder; +import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; +import org.jivesoftware.smackx.message_fastening.element.FasteningElement; + +/** + * Smacks API for XEP-0422: Message Fastening. + * The API is still very bare bones, as the XEP intends Message Fastening to be used as a tool by other protocols. + * + * To enable / disable auto-announcing support for this feature, call {@link #setEnabledByDefault(boolean)} (default true). + * + * To fasten a payload to a previous message, create an {@link FasteningElement} using the builder provided by + * {@link FasteningElement#builder()}. + * + * You need to provide the {@link org.jivesoftware.smackx.sid.element.OriginIdElement} of the message you want to reference. + * Then add wrapped payloads using {@link FasteningElement.Builder#addWrappedPayloads(List)} + * and external payloads using {@link FasteningElement.Builder#addExternalPayloads(List)}. + * + * If you fastened some payloads onto the message previously and now want to replace the previous fastening, call + * {@link FasteningElement.Builder#isRemovingElement()}. + * Once you are finished, build the {@link FasteningElement} using {@link FasteningElement.Builder#build()} and add it to + * a stanza by calling {@link FasteningElement#applyTo(MessageBuilder)}. + * + * @see XEP-0422: Message Fastening + */ +public final class MessageFasteningManager extends Manager { + + public static final String NAMESPACE = "urn:xmpp:fasten:0"; + + private static boolean ENABLED_BY_DEFAULT = false; + + private static final WeakHashMap INSTANCES = new WeakHashMap<>(); + + static { + XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() { + @Override + public void connectionCreated(XMPPConnection connection) { + if (ENABLED_BY_DEFAULT) { + MessageFasteningManager.getInstanceFor(connection).announceSupport(); + } + } + }); + } + + private MessageFasteningManager(XMPPConnection connection) { + super(connection); + } + + public static synchronized MessageFasteningManager getInstanceFor(XMPPConnection connection) { + MessageFasteningManager manager = INSTANCES.get(connection); + if (manager == null) { + manager = new MessageFasteningManager(connection); + INSTANCES.put(connection, manager); + } + return manager; + } + + /** + * Enable or disable auto-announcing support for Message Fastening. + * Default is enabled. + * + * @param enabled enabled + */ + public static synchronized void setEnabledByDefault(boolean enabled) { + ENABLED_BY_DEFAULT = enabled; + } + + /** + * Announce support for Message Fastening via Service Discovery. + */ + public void announceSupport() { + ServiceDiscoveryManager discoveryManager = ServiceDiscoveryManager.getInstanceFor(connection()); + discoveryManager.addFeature(NAMESPACE); + } + + /** + * Stop announcing support for Message Fastening. + */ + public void stopAnnouncingSupport() { + ServiceDiscoveryManager discoveryManager = ServiceDiscoveryManager.getInstanceFor(connection()); + discoveryManager.removeFeature(NAMESPACE); + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/element/ExternalElement.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/element/ExternalElement.java new file mode 100644 index 000000000..2a83352db --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/element/ExternalElement.java @@ -0,0 +1,84 @@ +/** + * + * Copyright 2019 Paul Schaub + * + * 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.message_fastening.element; + +import org.jivesoftware.smack.packet.NamedElement; +import org.jivesoftware.smack.packet.XmlEnvironment; +import org.jivesoftware.smack.util.XmlStringBuilder; + +/** + * Child element of {@link FasteningElement}. + * Reference to a top level element in the stanza that contains the {@link FasteningElement}. + */ +public class ExternalElement implements NamedElement { + + public static final String ELEMENT = "external"; + public static final String ATTR_NAME = "name"; + public static final String ATTR_ELEMENT_NAMESPACE = "element-namespace"; + + private final String name; + private final String elementNamespace; + + /** + * Create a new {@link ExternalElement} that references a top level element with the given name. + * + * @param name name of the top level element + */ + public ExternalElement(String name) { + this(name, null); + } + + /** + * Create a new {@link ExternalElement} that references a top level element with the given name and namespace. + * + * @param name name of the top level element + * @param elementNamespace namespace of the top level element + */ + public ExternalElement(String name, String elementNamespace) { + this.name = name; + this.elementNamespace = elementNamespace; + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public XmlStringBuilder toXML(XmlEnvironment xmlEnvironment) { + XmlStringBuilder xml = new XmlStringBuilder(this); + xml.attribute(ATTR_NAME, getName()); + xml.optAttribute(ATTR_ELEMENT_NAMESPACE, getElementNamespace()); + return xml.closeEmptyElement(); + } + + /** + * Name of the referenced top level element, eg. 'body'. + * @return element name + */ + public String getName() { + return name; + } + + /** + * Namespace of the referenced top level element, eg. 'urn:example:lik'. + * @return element namespace + */ + public String getElementNamespace() { + return elementNamespace; + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/element/FasteningElement.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/element/FasteningElement.java new file mode 100644 index 000000000..5328bd0b2 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/element/FasteningElement.java @@ -0,0 +1,325 @@ +/** + * + * Copyright 2019 Paul Schaub + * + * 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.message_fastening.element; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.jivesoftware.smack.packet.ExtensionElement; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.MessageBuilder; +import org.jivesoftware.smack.packet.Stanza; +import org.jivesoftware.smack.packet.XmlEnvironment; +import org.jivesoftware.smack.util.Objects; +import org.jivesoftware.smack.util.XmlStringBuilder; +import org.jivesoftware.smackx.message_fastening.MessageFasteningManager; +import org.jivesoftware.smackx.sid.element.OriginIdElement; + +/** + * Message Fastening container element. + */ +public final class FasteningElement implements ExtensionElement { + + public static final String ELEMENT = "apply-to"; + public static final String NAMESPACE = MessageFasteningManager.NAMESPACE; + public static final String ATTR_ID = "id"; + public static final String ATTR_CLEAR = "clear"; + public static final String ATTR_SHELL = "shell"; + + private final OriginIdElement referencedStanzasOriginId; + private final List externalPayloads = new ArrayList<>(); + private final List wrappedPayloads = new ArrayList<>(); + private final boolean clear; + private final boolean shell; + + private FasteningElement(OriginIdElement originId, + List wrappedPayloads, + List externalPayloads, + boolean clear, + boolean shell) { + this.referencedStanzasOriginId = Objects.requireNonNull(originId, "Fastening element MUST contain an origin-id."); + this.wrappedPayloads.addAll(wrappedPayloads); + this.externalPayloads.addAll(externalPayloads); + this.clear = clear; + this.shell = shell; + } + + /** + * Return the {@link OriginIdElement origin-id} of the {@link Stanza} that the message fastenings are to be + * applied to. + * + * @return origin id of the referenced stanza + */ + public OriginIdElement getReferencedStanzasOriginId() { + return referencedStanzasOriginId; + } + + /** + * Return all wrapped payloads of this element. + * + * @see XEP-0422: §3.1. Wrapped Payloads + * + * @return wrapped payloads. + */ + public List getWrappedPayloads() { + return Collections.unmodifiableList(wrappedPayloads); + } + + /** + * Return all external payloads of this element. + * + * @see XEP-0422: §3.2. External Payloads + * + * @return external payloads. + */ + public List getExternalPayloads() { + return Collections.unmodifiableList(externalPayloads); + } + + /** + * Does this element remove a previously sent {@link FasteningElement}? + * + * @see + * XEP-0422: Message Fastening §3.4 Removing fastenings + * + * @return true if the clear attribute is set. + */ + public boolean isRemovingElement() { + return clear; + } + + /** + * Is this a shell element? + * Shell elements are otherwise empty elements that indicate that an encrypted payload of a message + * encrypted using XEP-420: Stanza Content Encryption contains a sensitive {@link FasteningElement}. + * + * @see + * XEP-0422: Message Fastening §3.5 Interaction with stanza encryption + * + * @return true if this is a shell element. + */ + public boolean isShellElement() { + return shell; + } + + /** + * Return true if the provided {@link Message} contains a {@link FasteningElement}. + * + * @param message message + * @return true if the stanza has an {@link FasteningElement}. + */ + public static boolean hasFasteningElement(Message message) { + return message.hasExtension(ELEMENT, MessageFasteningManager.NAMESPACE); + } + + /** + * Return true if the provided {@link MessageBuilder} contains a {@link FasteningElement}. + * + * @param builder message builder + * @return true if the stanza has an {@link FasteningElement}. + */ + public static boolean hasFasteningElement(MessageBuilder builder) { + return builder.hasExtension(FasteningElement.class); + } + + @Override + public String getNamespace() { + return MessageFasteningManager.NAMESPACE; + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public XmlStringBuilder toXML(XmlEnvironment xmlEnvironment) { + XmlStringBuilder xml = new XmlStringBuilder(this) + .attribute(ATTR_ID, referencedStanzasOriginId.getId()) + .optBooleanAttribute(ATTR_CLEAR, isRemovingElement()) + .optBooleanAttribute(ATTR_SHELL, isShellElement()) + .rightAngleBracket(); + addPayloads(xml); + return xml.closeElement(this); + } + + private void addPayloads(XmlStringBuilder xml) { + for (ExternalElement external : externalPayloads) { + xml.append(external); + } + for (ExtensionElement wrapped : wrappedPayloads) { + xml.append(wrapped); + } + } + + public static FasteningElement createShellElementForSensitiveElement(FasteningElement sensitiveElement) { + return createShellElementForSensitiveElement(sensitiveElement.getReferencedStanzasOriginId()); + } + + public static FasteningElement createShellElementForSensitiveElement(String originIdOfSensitiveElement) { + return createShellElementForSensitiveElement(new OriginIdElement(originIdOfSensitiveElement)); + } + + public static FasteningElement createShellElementForSensitiveElement(OriginIdElement originIdOfSensitiveElement) { + return FasteningElement.builder() + .setOriginId(originIdOfSensitiveElement) + .setShell() + .build(); + } + + /** + * Add this element to the provided message builder. + * Note: The stanza MUST NOT contain more than one apply-to elements at the same time. + * + * @see XEP-0422 §4: Business Rules + * + * @param messageBuilder message builder + */ + public void applyTo(MessageBuilder messageBuilder) { + if (FasteningElement.hasFasteningElement(messageBuilder)) { + throw new IllegalArgumentException("Stanza cannot contain more than one apply-to elements."); + } else { + messageBuilder.addExtension(this); + } + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private OriginIdElement originId; + private final List wrappedPayloads = new ArrayList<>(); + private final List externalPayloads = new ArrayList<>(); + private boolean isClear = false; + private boolean isShell = false; + + /** + * Set the origin-id of the referenced message. + * + * @param originIdString origin id as String + * @return builder instance + */ + public Builder setOriginId(String originIdString) { + return setOriginId(new OriginIdElement(originIdString)); + } + + /** + * Set the {@link OriginIdElement} of the referenced message. + * + * @param originId origin-id as element + * @return builder instance + */ + public Builder setOriginId(OriginIdElement originId) { + this.originId = originId; + return this; + } + + /** + * Add a wrapped payload. + * + * @param wrappedPayload wrapped payload + * @return builder instance + */ + public Builder addWrappedPayload(ExtensionElement wrappedPayload) { + return addWrappedPayloads(Collections.singletonList(wrappedPayload)); + } + + /** + * Add multiple wrapped payloads at once. + * + * @param wrappedPayloads list of wrapped payloads + * @return builder instance + */ + public Builder addWrappedPayloads(List wrappedPayloads) { + this.wrappedPayloads.addAll(wrappedPayloads); + return this; + } + + /** + * Add an external payload. + * + * @param externalPayload external payload + * @return builder instance + */ + public Builder addExternalPayload(ExternalElement externalPayload) { + return addExternalPayloads(Collections.singletonList(externalPayload)); + } + + /** + * Add multiple external payloads at once. + * + * @param externalPayloads external payloads + * @return builder instance + */ + public Builder addExternalPayloads(List externalPayloads) { + this.externalPayloads.addAll(externalPayloads); + return this; + } + + /** + * Declare this {@link FasteningElement} to remove previous fastenings. + * Semantically the wrapped payloads of this element declares all wrapped payloads from the referenced + * fastening element that share qualified names as removed. + * + * @see + * XEP-0422: Message Fastening §3.4 Removing fastenings + * + * @return builder instance + */ + public Builder setClear() { + isClear = true; + return this; + } + + /** + * Declare this {@link FasteningElement} to be a shell element. + * Shell elements are used as hints that a Stanza Content Encryption payload contains another sensitive + * {@link FasteningElement}. The outer "shell" {@link FasteningElement} is used to do fastening collation. + * + * @see XEP-0422: Message Fastening §3.5 Interaction with stanza encryption + * @see XEP-0420: Stanza Content Encryption + * + * @return builder instance + */ + public Builder setShell() { + isShell = true; + return this; + } + + /** + * Build the element. + * @return built element. + */ + public FasteningElement build() { + validateThatIfIsShellThenOtherwiseEmpty(); + return new FasteningElement(originId, wrappedPayloads, externalPayloads, isClear, isShell); + } + + private void validateThatIfIsShellThenOtherwiseEmpty() { + if (!isShell) { + return; + } + + if (isClear || !wrappedPayloads.isEmpty() || !externalPayloads.isEmpty()) { + throw new IllegalArgumentException("A fastening that is a shell element must be otherwise empty " + + "and cannot have a 'clear' attribute."); + } + } + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/element/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/element/package-info.java new file mode 100644 index 000000000..8ae0915c7 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/element/package-info.java @@ -0,0 +1,25 @@ +/** + * + * Copyright 2019 Paul Schaub + * + * 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. + */ + +/** + * XEP-0422: Message Fastening. + * + * @see XEP-0422: Message + * Fastening + * + */ +package org.jivesoftware.smackx.message_fastening.element; diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/package-info.java new file mode 100644 index 000000000..90dba9915 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/package-info.java @@ -0,0 +1,25 @@ +/** + * + * Copyright 2019 Paul Schaub + * + * 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. + */ + +/** + * XEP-0422: Message Fastening. + * + * @see XEP-0422: Message + * Fastening + * + */ +package org.jivesoftware.smackx.message_fastening; diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/provider/FasteningElementProvider.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/provider/FasteningElementProvider.java new file mode 100644 index 000000000..7005bda99 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/provider/FasteningElementProvider.java @@ -0,0 +1,80 @@ +/** + * + * Copyright 2019 Paul Schaub + * + * 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.message_fastening.provider; + +import java.io.IOException; + +import org.jivesoftware.smack.packet.ExtensionElement; +import org.jivesoftware.smack.packet.XmlEnvironment; +import org.jivesoftware.smack.parsing.SmackParsingException; +import org.jivesoftware.smack.provider.ExtensionElementProvider; +import org.jivesoftware.smack.util.PacketParserUtils; +import org.jivesoftware.smack.util.ParserUtils; +import org.jivesoftware.smack.xml.XmlPullParser; +import org.jivesoftware.smack.xml.XmlPullParserException; +import org.jivesoftware.smackx.message_fastening.MessageFasteningManager; +import org.jivesoftware.smackx.message_fastening.element.ExternalElement; +import org.jivesoftware.smackx.message_fastening.element.FasteningElement; + +public class FasteningElementProvider extends ExtensionElementProvider { + + public static final FasteningElementProvider TEST_INSTANCE = new FasteningElementProvider(); + + @Override + public FasteningElement parse(XmlPullParser parser, int initialDepth, XmlEnvironment xmlEnvironment) throws XmlPullParserException, IOException, SmackParsingException { + FasteningElement.Builder builder = FasteningElement.builder(); + builder.setOriginId(parser.getAttributeValue("", FasteningElement.ATTR_ID)); + if (ParserUtils.getBooleanAttribute(parser, FasteningElement.ATTR_CLEAR, false)) { + builder.setClear(); + } + if (ParserUtils.getBooleanAttribute(parser, FasteningElement.ATTR_SHELL, false)) { + builder.setShell(); + } + + outerloop: while (true) { + XmlPullParser.Event tag = parser.next(); + switch (tag) { + case START_ELEMENT: + String name = parser.getName(); + String namespace = parser.getNamespace(); + + // Parse external payload + if (MessageFasteningManager.NAMESPACE.equals(namespace) && ExternalElement.ELEMENT.equals(name)) { + ExternalElement external = new ExternalElement( + parser.getAttributeValue("", ExternalElement.ATTR_NAME), + parser.getAttributeValue("", ExternalElement.ATTR_ELEMENT_NAMESPACE)); + builder.addExternalPayload(external); + continue; + } + + // Parse wrapped payload + ExtensionElement wrappedPayload = PacketParserUtils.parseExtensionElement(name, namespace, parser, xmlEnvironment); + builder.addWrappedPayload(wrappedPayload); + break; + + case END_ELEMENT: + if (parser.getDepth() == initialDepth) { + break outerloop; + } + break; + default: + break; + } + } + return builder.build(); + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/provider/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/provider/package-info.java new file mode 100644 index 000000000..cf2fcf1ff --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/provider/package-info.java @@ -0,0 +1,25 @@ +/** + * + * Copyright 2019 Paul Schaub + * + * 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. + */ + +/** + * XEP-0422: Message Fastening. + * + * @see XEP-0422: Message + * Fastening + * + */ +package org.jivesoftware.smackx.message_fastening.provider; diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/push_notifications/element/EnablePushNotificationsIQ.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/push_notifications/element/EnablePushNotificationsIQ.java index 7c86584d8..428b41b67 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/push_notifications/element/EnablePushNotificationsIQ.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/push_notifications/element/EnablePushNotificationsIQ.java @@ -24,6 +24,7 @@ import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smackx.pubsub.packet.PubSub; import org.jivesoftware.smackx.xdata.FormField; +import org.jivesoftware.smackx.xdata.TextSingleFormField; import org.jivesoftware.smackx.xdata.packet.DataForm; import org.jxmpp.jid.Jid; @@ -98,24 +99,23 @@ public class EnablePushNotificationsIQ extends IQ { xml.rightAngleBracket(); 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 // '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. - FormField.Builder formTypeField = FormField.builder("FORM_TYPE"); - formTypeField.addValue(PubSub.NAMESPACE + "#publish-options"); - dataForm.addField(formTypeField.build()); + FormField formTypeField = FormField.buildHiddenFormType(PubSub.NAMESPACE + "#publish-options"); + dataForm.addField(formTypeField); Iterator> publishOptionsIterator = publishOptions.entrySet().iterator(); while (publishOptionsIterator.hasNext()) { Map.Entry pairVariableValue = publishOptionsIterator.next(); - FormField.Builder field = FormField.builder(pairVariableValue.getKey()); - field.addValue(pairVariableValue.getValue()); + TextSingleFormField.Builder field = FormField.builder(pairVariableValue.getKey()); + field.setValue(pairVariableValue.getValue()); dataForm.addField(field.build()); } - xml.append(dataForm); + xml.append(dataForm.build()); } return xml; diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/sid/element/OriginIdElement.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/sid/element/OriginIdElement.java index 585be47a6..b157b40a0 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/sid/element/OriginIdElement.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/sid/element/OriginIdElement.java @@ -101,4 +101,25 @@ public class OriginIdElement extends StableAndUniqueIdElement { .attribute(ATTR_ID, getId()) .closeEmptyElement(); } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null) { + return false; + } + if (!(other instanceof OriginIdElement)) { + return false; + } + + OriginIdElement otherId = (OriginIdElement) other; + return getId().equals(otherId.getId()); + } + + @Override + public int hashCode() { + return getId().hashCode(); + } } diff --git a/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers b/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers index 81c36ee64..d594578a4 100644 --- a/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers +++ b/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers @@ -292,6 +292,12 @@ org.jivesoftware.smackx.dox.provider.DnsIqProvider + + + apply-to + urn:xmpp:fasten:0 + org.jivesoftware.smackx.message_fastening.provider.FasteningElementProvider + diff --git a/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.xml b/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.xml index 347a698f3..6a4f6a759 100644 --- a/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.xml +++ b/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.xml @@ -8,5 +8,6 @@ org.jivesoftware.smackx.eme.ExplicitMessageEncryptionManager org.jivesoftware.smackx.sid.StableUniqueStanzaIdManager org.jivesoftware.smackx.xmlelement.DataFormsXmlElementManager + org.jivesoftware.smackx.message_fastening.MessageFasteningManager diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/FiltersTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/FiltersTest.java index 738906053..212c37e89 100644 --- a/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/FiltersTest.java +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/FiltersTest.java @@ -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"); * 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 { private static String getMamXMemberWith(List fieldsNames, List fieldsValues) { - String xml = "" + "" + "" + String xml = "" + "" + "" + MamElements.NAMESPACE + "" + ""; for (int i = 0; i < fieldsNames.size() && i < fieldsValues.size(); i++) { diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/MamQueryIQProviderTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/MamQueryIQProviderTest.java index f39411a00..c9f3be99e 100644 --- a/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/MamQueryIQProviderTest.java +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/MamQueryIQProviderTest.java @@ -85,7 +85,7 @@ public class MamQueryIQProviderTest { assertEquals(fields2.get(1).getType(), FormField.Type.jid_single); assertEquals(fields2.get(2).getType(), FormField.Type.text_single); 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"); } } diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/MamTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/MamTest.java index 6dd2f8e73..33e9df1a3 100644 --- a/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/MamTest.java +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/MamTest.java @@ -49,7 +49,8 @@ public class MamTest extends SmackTestSuite { IllegalArgumentException, InvocationTargetException { Method methodGetNewMamForm = MamManager.class.getDeclaredMethod("getNewMamForm"); methodGetNewMamForm.setAccessible(true); - return (DataForm) methodGetNewMamForm.invoke(mamManager); + DataForm.Builder dataFormBuilder = (DataForm.Builder) methodGetNewMamForm.invoke(mamManager); + return dataFormBuilder.build(); } } diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/PagingTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/PagingTest.java index d0919dbcf..1f1450efd 100644 --- a/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/PagingTest.java +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/PagingTest.java @@ -30,7 +30,7 @@ import org.junit.jupiter.api.Test; public class PagingTest extends MamTest { private static final String pagingStanza = "" + "" - + "" + "" + + "" + "" + "urn:xmpp:mam:1" + "" + "" + "" + "10" + "" + "" + ""; diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/QueryArchiveTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/QueryArchiveTest.java index bc4360eef..8fe38eeec 100644 --- a/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/QueryArchiveTest.java +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/QueryArchiveTest.java @@ -41,7 +41,7 @@ import org.jxmpp.jid.impl.JidCreate; public class QueryArchiveTest extends MamTest { private static final String mamSimpleQueryIQ = "" + "" - + "" + "" + "" + + "" + "" + "" + MamElements.NAMESPACE + "" + "" + "" + "" + ""; private static final String mamQueryResultExample = "" diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/ResultsLimitTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/ResultsLimitTest.java index 9adebb3d5..157abc520 100644 --- a/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/ResultsLimitTest.java +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/ResultsLimitTest.java @@ -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"); * 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 { private static final String resultsLimitStanza = "" + "" - + "" + "" + "" + + "" + "" + "" + MamElements.NAMESPACE + "" + "" + "" + "" + "10" + "" + "" + ""; diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/RetrieveFormFieldsTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/RetrieveFormFieldsTest.java index 264319c0e..87000a6e8 100644 --- a/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/RetrieveFormFieldsTest.java +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/RetrieveFormFieldsTest.java @@ -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"); * 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.junit.jupiter.api.Test; +import org.jxmpp.jid.JidTestUtil; public class RetrieveFormFieldsTest extends MamTest { private static final String retrieveFormFieldStanza = "" + "" + ""; - private static final String additionalFieldsStanza = "" + "" + private static final String additionalFieldsStanza = "" + "" + "" + MamElements.NAMESPACE + "" + "" + "" + "Hi" + "" - + "" + "Hi2" + "" + + "" + "one@exampleone.org" + "" + ""; @Test @@ -51,13 +52,11 @@ public class RetrieveFormFieldsTest extends MamTest { @Test public void checkAddAdditionalFieldsStanza() throws Exception { FormField field1 = FormField.builder("urn:example:xmpp:free-text-search") - .setType(FormField.Type.text_single) - .addValue("Hi") + .setValue("Hi") .build(); - FormField field2 = FormField.builder("urn:example:xmpp:stanza-content") - .setType(FormField.Type.jid_single) - .addValue("Hi2") + FormField field2 = FormField.jidSingleBuilder("urn:example:xmpp:stanza-content") + .setValue(JidTestUtil.BARE_JID_1) .build(); MamQueryArgs mamQueryArgs = MamQueryArgs.builder() diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/message_fastening/MessageFasteningElementsTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/message_fastening/MessageFasteningElementsTest.java new file mode 100644 index 000000000..8228fccfb --- /dev/null +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/message_fastening/MessageFasteningElementsTest.java @@ -0,0 +1,229 @@ +/** + * + * Copyright 2019 Paul Schaub + * + * 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.message_fastening; + +import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.util.Arrays; + +import org.jivesoftware.smack.packet.MessageBuilder; +import org.jivesoftware.smack.packet.StandardExtensionElement; +import org.jivesoftware.smack.packet.StanzaFactory; +import org.jivesoftware.smack.packet.id.StandardStanzaIdSource; +import org.jivesoftware.smack.parsing.SmackParsingException; +import org.jivesoftware.smack.test.util.SmackTestUtil; +import org.jivesoftware.smack.test.util.TestUtils; +import org.jivesoftware.smack.xml.XmlPullParserException; +import org.jivesoftware.smackx.message_fastening.element.ExternalElement; +import org.jivesoftware.smackx.message_fastening.element.FasteningElement; +import org.jivesoftware.smackx.message_fastening.provider.FasteningElementProvider; +import org.jivesoftware.smackx.sid.element.OriginIdElement; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +public class MessageFasteningElementsTest { + + private final StanzaFactory stanzaFactory = new StanzaFactory(new StandardStanzaIdSource()); + + /** + * Test XML serialization of the {@link FasteningElement} using the example provided by + * the XEP. + * + * @see XEP-0422 §3.1 Wrapped Payloads + */ + @Test + public void fasteningElementSerializationTest() { + String xml = + "" + + " " + + ""; + + FasteningElement applyTo = FasteningElement.builder() + .setOriginId("origin-id-1") + .addWrappedPayload(new StandardExtensionElement("i-like-this", "urn:example:like")) + .build(); + + assertXmlSimilar(xml, applyTo.toXML().toString()); + } + + @ParameterizedTest + @EnumSource(SmackTestUtil.XmlPullParserKind.class) + public void fasteningDeserializationTest(SmackTestUtil.XmlPullParserKind parserKind) throws XmlPullParserException, IOException, SmackParsingException { + String xml = + "" + + " " + + " " + + " " + + ""; + + FasteningElement parsed = SmackTestUtil.parse(xml, FasteningElementProvider.class, parserKind); + + assertNotNull(parsed); + assertEquals(new OriginIdElement("origin-id-1"), parsed.getReferencedStanzasOriginId()); + assertFalse(parsed.isRemovingElement()); + assertFalse(parsed.isShellElement()); + + assertEquals(1, parsed.getWrappedPayloads().size()); + assertEquals("i-like-this", parsed.getWrappedPayloads().get(0).getElementName()); + assertEquals("urn:example:like", parsed.getWrappedPayloads().get(0).getNamespace()); + + assertEquals(2, parsed.getExternalPayloads().size()); + ExternalElement custom = parsed.getExternalPayloads().get(0); + assertEquals("custom", custom.getName()); + assertEquals("urn:example:custom", custom.getElementNamespace()); + ExternalElement body = parsed.getExternalPayloads().get(1); + assertEquals("body", body.getName()); + assertNull(body.getElementNamespace()); + } + + @Test + public void fasteningDeserializationClearTest() throws XmlPullParserException, IOException, SmackParsingException { + String xml = + "" + + " " + + ""; + + FasteningElement parsed = FasteningElementProvider.TEST_INSTANCE.parse(TestUtils.getParser(xml)); + + assertTrue(parsed.isRemovingElement()); + } + + @Test + public void fasteningElementWithExternalElementsTest() { + String xml = + "" + + " " + + " " + + " " + + ""; + + FasteningElement element = FasteningElement.builder() + .setOriginId("origin-id-2") + .addExternalPayloads(Arrays.asList( + new ExternalElement("body"), + new ExternalElement("custom", "urn:example:custom") + )) + .addWrappedPayload( + new StandardExtensionElement("edit", "urn:example.edit")) + .build(); + + assertXmlSimilar(xml, element.toXML().toString()); + } + + @Test + public void createShellElementSharesOriginIdTest() { + OriginIdElement originIdElement = new OriginIdElement("sensitive-stanza-1"); + FasteningElement sensitiveFastening = FasteningElement.builder() + .setOriginId(originIdElement) + .build(); + + FasteningElement shellElement = FasteningElement.createShellElementForSensitiveElement(sensitiveFastening); + + assertEquals(originIdElement, shellElement.getReferencedStanzasOriginId()); + } + + @Test + public void fasteningRemoveSerializationTest() { + String xml = + "" + + " Very much" + + ""; + + FasteningElement element = FasteningElement.builder() + .setOriginId("origin-id-1") + .setClear() + .addWrappedPayload(StandardExtensionElement.builder("i-like-this", "urn:example:like") + .setText("Very much") + .build()) + .build(); + + assertXmlSimilar(xml, element.toXML().toString()); + } + + @Test + public void hasFasteningElementTest() { + MessageBuilder messageBuilderWithFasteningElement = MessageBuilder.buildMessage() + .setBody("Hi!") + .addExtension(FasteningElement.builder().setOriginId("origin-id-1").build()); + MessageBuilder messageBuilderWithoutFasteningElement = MessageBuilder.buildMessage() + .setBody("Ho!"); + + assertTrue(FasteningElement.hasFasteningElement(messageBuilderWithFasteningElement)); + assertFalse(FasteningElement.hasFasteningElement(messageBuilderWithoutFasteningElement)); + } + + @Test + public void shellElementMustNotHaveClearAttributeTest() { + assertThrows(IllegalArgumentException.class, () -> + FasteningElement.builder() + .setShell() + .setClear() + .build()); + } + + @Test + public void shellElementMustNotContainAnyPayloads() { + assertThrows(IllegalArgumentException.class, () -> + FasteningElement.builder() + .setShell() + .addWrappedPayload(new StandardExtensionElement("edit", "urn:example.edit")) + .build()); + + assertThrows(IllegalArgumentException.class, () -> + FasteningElement.builder() + .setShell() + .addExternalPayload(new ExternalElement("body")) + .build()); + } + + @Test + public void ensureAddFasteningElementToStanzaWorks() { + MessageBuilder message = stanzaFactory.buildMessageStanza(); + FasteningElement fasteningElement = FasteningElement.builder().setOriginId("another-apply-to").build(); + + // Adding only one element is allowed + fasteningElement.applyTo(message); + } + + /** + * Ensure, that {@link FasteningElement#applyTo(MessageBuilder)} + * throws when trying to add an {@link FasteningElement} to a {@link MessageBuilder} that already contains one + * such element. + * + * @see XEP-0422: §4. Business Rules + */ + @Test + public void ensureStanzaCanOnlyContainOneFasteningElement() { + MessageBuilder messageWithFastening = stanzaFactory.buildMessageStanza(); + FasteningElement.builder().setOriginId("origin-id").build().applyTo(messageWithFastening); + + // Adding a second fastening MUST result in exception + Assertions.assertThrows(IllegalArgumentException.class, () -> + FasteningElement.builder().setOriginId("another-apply-to").build() + .applyTo(messageWithFastening)); + } +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/admin/ServiceAdministrationManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/admin/ServiceAdministrationManager.java index a333b2027..78ce687a6 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/admin/ServiceAdministrationManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/admin/ServiceAdministrationManager.java @@ -1,6 +1,6 @@ /** * - * Copyright 2016-2019 Florian Schmaus + * Copyright 2016-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. @@ -29,7 +29,7 @@ import org.jivesoftware.smack.XMPPException.XMPPErrorException; import org.jivesoftware.smackx.commands.AdHocCommandManager; 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.Jid; @@ -72,7 +72,7 @@ public class ServiceAdministrationManager extends Manager { RemoteCommand command = addUser(); command.execute(); - Form answerForm = command.getForm().createAnswerForm(); + FillableForm answerForm = new FillableForm(command.getForm()); answerForm.setAnswer("accountjid", userJid); answerForm.setAnswer("password", password); @@ -101,7 +101,7 @@ public class ServiceAdministrationManager extends Manager { RemoteCommand command = deleteUser(); command.execute(); - Form answerForm = command.getForm().createAnswerForm(); + FillableForm answerForm = new FillableForm(command.getForm()); answerForm.setAnswer("accountjids", jidsToDelete); diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/caps/EntityCapsManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/caps/EntityCapsManager.java index 6591d7512..5407d11e3 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/caps/EntityCapsManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/caps/EntityCapsManager.java @@ -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"); * you may not use this file except in compliance with the License. @@ -19,13 +19,16 @@ package org.jivesoftware.smackx.caps; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.Collections; import java.util.Comparator; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Queue; +import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.WeakHashMap; @@ -48,7 +51,6 @@ import org.jivesoftware.smack.filter.PresenceTypeFilter; import org.jivesoftware.smack.filter.StanzaExtensionFilter; import org.jivesoftware.smack.filter.StanzaFilter; import org.jivesoftware.smack.filter.StanzaTypeFilter; -import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.packet.PresenceBuilder; @@ -548,7 +550,7 @@ public final class EntityCapsManager extends Manager { final List identities = new LinkedList<>(ServiceDiscoveryManager.getInstanceFor(connection).getIdentities()); sdm.setNodeInformationProvider(localNodeVer, new AbstractNodeInformationProvider() { List features = sdm.getFeatures(); - List packetExtensions = sdm.getExtendedInfoAsList(); + List packetExtensions = sdm.getExtendedInfo(); @Override public List getNodeFeatures() { return features; @@ -558,7 +560,7 @@ public final class EntityCapsManager extends Manager { return identities; } @Override - public List getNodePacketExtensions() { + public List getNodePacketExtensions() { return packetExtensions; } }); @@ -600,7 +602,7 @@ public final class EntityCapsManager extends Manager { return false; // step 3.5 check for well-formed packet extensions - if (verifyPacketExtensions(info)) + if (!verifyPacketExtensions(info)) return false; String calculatedVer = generateVerificationString(info, hash).version; @@ -612,27 +614,29 @@ public final class EntityCapsManager extends Manager { } /** + * Verify that the given discovery info is not ill-formed. * - * @param info TODO javadoc me please - * @return true if the stanza extensions is ill-formed + * @param info the discovery info to verify. + * @return true if the stanza extensions is not ill-formed */ - protected static boolean verifyPacketExtensions(DiscoverInfo info) { - List foundFormTypes = new LinkedList<>(); - for (ExtensionElement pe : info.getExtensions()) { - if (pe.getNamespace().equals(DataForm.NAMESPACE)) { - DataForm df = (DataForm) pe; - for (FormField f : df.getFields()) { - if (f.getVariable().equals("FORM_TYPE")) { - for (FormField fft : foundFormTypes) { - if (f.equals(fft)) - return true; - } - foundFormTypes.add(f); - } - } + private static boolean verifyPacketExtensions(DiscoverInfo info) { + Set foundFormTypes = new HashSet<>(); + List dataForms = info.getExtensions(DataForm.class); + for (DataForm dataForm : dataForms) { + FormField formFieldTypeField = dataForm.getHiddenFormTypeField(); + if (formFieldTypeField == null) { + continue; + } + + 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) { @@ -664,8 +668,6 @@ public final class EntityCapsManager extends Manager { // be "broken" implementation in the wild, so we *always* transform to lowercase. hash = hash.toLowerCase(Locale.US); - DataForm extendedInfo = DataForm.from(discoverInfo); - // 1. Initialize an empty string S ('sb' in this method). StringBuilder sb = new StringBuilder(); // Use StringBuilder as we don't // need thread-safe StringBuffer @@ -705,50 +707,47 @@ public final class EntityCapsManager extends Manager { sb.append('<'); } - // only use the data form for calculation is it has a hidden FORM_TYPE - // field - // see XEP-0115 5.4 step 3.6 - if (extendedInfo != null && extendedInfo.hasHiddenFormTypeField()) { - synchronized (extendedInfo) { - // 6. If the service discovery information response includes - // XEP-0128 data forms, sort the forms by the FORM_TYPE (i.e., - // by the XML character data of the element). - SortedSet fs = new TreeSet<>(new Comparator() { - @Override - public int compare(FormField f1, FormField f2) { - return f1.getVariable().compareTo(f2.getVariable()); - } - }); + List extendedInfos = discoverInfo.getExtensions(DataForm.class); + for (DataForm extendedInfo : extendedInfos) { + if (!extendedInfo.hasHiddenFormTypeField()) { + // Only use the data form for calculation is it has a hidden FORM_TYPE field. + // See XEP-0115 5.4 step 3.f + continue; + } - FormField ft = null; - - for (FormField f : extendedInfo.getFields()) { - if (!f.getVariable().equals("FORM_TYPE")) { - fs.add(f); - } else { - ft = f; - } + // 6. If the service discovery information response includes + // XEP-0128 data forms, sort the forms by the FORM_TYPE (i.e., + // by the XML character data of the element). + SortedSet fs = new TreeSet<>(new Comparator() { + @Override + public int compare(FormField f1, FormField f2) { + return f1.getFieldName().compareTo(f2.getFieldName()); } + }); - // Add FORM_TYPE values - if (ft != null) { - formFieldValuesToCaps(ft.getValues(), sb); - } - - // 7. 3. For each field other than FORM_TYPE: - // 1. Append the value of the "var" attribute, followed by the - // '<' character. - // 2. Sort values by the XML character data of the - // element. - // 3. For each element, append the XML character data, - // followed by the '<' character. - for (FormField f : fs) { - sb.append(f.getVariable()); - sb.append('<'); - formFieldValuesToCaps(f.getValues(), sb); + for (FormField f : extendedInfo.getFields()) { + if (!f.getFieldName().equals("FORM_TYPE")) { + fs.add(f); } } + + // Add FORM_TYPE values + formFieldValuesToCaps(Collections.singletonList(extendedInfo.getFormType()), sb); + + // 7. 3. For each field other than FORM_TYPE: + // 1. Append the value of the "var" attribute, followed by the + // '<' character. + // 2. Sort values by the XML character data of the + // element. + // 3. For each element, append the XML character data, + // followed by the '<' character. + for (FormField f : fs) { + sb.append(f.getFieldName()); + sb.append('<'); + formFieldValuesToCaps(f.getValues(), sb); + } } + // 8. Ensure that S is encoded according to the UTF-8 encoding (RFC // 3269). // 9. Compute the verification string by hashing S using the algorithm @@ -767,7 +766,7 @@ public final class EntityCapsManager extends Manager { return new CapsVersionAndHash(version, hash); } - private static void formFieldValuesToCaps(List i, StringBuilder sb) { + private static void formFieldValuesToCaps(List i, StringBuilder sb) { SortedSet fvs = new TreeSet<>(); fvs.addAll(i); for (CharSequence fv : fvs) { diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/AdHocCommand.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/AdHocCommand.java index 32f6eea41..c7815fa8c 100755 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/AdHocCommand.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/AdHocCommand.java @@ -24,7 +24,8 @@ import org.jivesoftware.smack.XMPPException.XMPPErrorException; import org.jivesoftware.smack.packet.StanzaError; 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; @@ -188,13 +189,8 @@ public abstract class AdHocCommand { * @return the form of the current stage to fill out or the result of the * execution. */ - public Form getForm() { - if (data.getForm() == null) { - return null; - } - else { - return new Form(data.getForm()); - } + public DataForm getForm() { + return 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 * execution. */ - protected void setForm(Form form) { - data.setForm(form.getDataFormToSend()); + protected void setForm(DataForm form) { + data.setForm(form); } /** @@ -234,7 +230,7 @@ public abstract class AdHocCommand { * @throws NotConnectedException if the XMPP connection is not connected. * @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 @@ -250,7 +246,7 @@ public abstract class AdHocCommand { * @throws NotConnectedException if the XMPP connection is not connected. * @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 diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/AdHocCommandManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/AdHocCommandManager.java index 588ca2116..0f4614181 100755 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/AdHocCommandManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/AdHocCommandManager.java @@ -52,7 +52,8 @@ import org.jivesoftware.smackx.disco.AbstractNodeInformationProvider; import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; import org.jivesoftware.smackx.disco.packet.DiscoverInfo; 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; @@ -462,7 +463,8 @@ public final class AdHocCommandManager extends Manager { if (Action.next.equals(action)) { command.incrementStage(); - command.next(new Form(requestData.getForm())); + DataForm dataForm = requestData.getForm(); + command.next(new FillableForm(dataForm)); if (command.isLastStage()) { // If it is the last stage then the command is // completed @@ -475,7 +477,8 @@ public final class AdHocCommandManager extends Manager { } else if (Action.complete.equals(action)) { command.incrementStage(); - command.complete(new Form(requestData.getForm())); + DataForm dataForm = requestData.getForm(); + command.complete(new FillableForm(dataForm)); response.setStatus(Status.completed); // Remove the completed session executingCommands.remove(sessionId); diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/RemoteCommand.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/RemoteCommand.java index f31e2a317..58d8cc74e 100755 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/RemoteCommand.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/RemoteCommand.java @@ -23,7 +23,8 @@ import org.jivesoftware.smack.XMPPException.XMPPErrorException; import org.jivesoftware.smack.packet.IQ; 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; @@ -80,8 +81,8 @@ public class RemoteCommand extends AdHocCommand { } @Override - public void complete(Form form) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { - executeAction(Action.complete, form); + public void complete(FillableForm form) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { + executeAction(Action.complete, form.getDataFormToSubmit()); } @Override @@ -100,13 +101,13 @@ public class RemoteCommand extends AdHocCommand { * @throws NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. */ - public void execute(Form form) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { - executeAction(Action.execute, form); + public void execute(FillableForm form) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { + executeAction(Action.execute, form.getDataFormToSubmit()); } @Override - public void next(Form form) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { - executeAction(Action.next, form); + public void next(FillableForm form) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { + executeAction(Action.next, form.getDataFormToSubmit()); } @Override @@ -130,7 +131,7 @@ public class RemoteCommand extends AdHocCommand { * @throws NotConnectedException if the XMPP connection is not connected. * @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: not throw the corresponding exception. This will make a faster response, // TODO: since the request is stopped before it's sent. @@ -140,10 +141,7 @@ public class RemoteCommand extends AdHocCommand { data.setNode(getNode()); data.setSessionID(sessionID); data.setAction(action); - - if (form != null) { - data.setForm(form.getDataFormToSend()); - } + data.setForm(form); AdHocCommandData responseData = null; try { diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/AbstractNodeInformationProvider.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/AbstractNodeInformationProvider.java index 79b96a11b..cfd51526c 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/AbstractNodeInformationProvider.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/AbstractNodeInformationProvider.java @@ -1,6 +1,6 @@ /** * - * Copyright © 2014 Florian Schmaus + * Copyright © 2014-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. @@ -42,7 +42,7 @@ public abstract class AbstractNodeInformationProvider implements NodeInformation } @Override - public List getNodePacketExtensions() { + public List getNodePacketExtensions() { return null; } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/NodeInformationProvider.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/NodeInformationProvider.java index 3b2587d2e..95961d140 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/NodeInformationProvider.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/NodeInformationProvider.java @@ -68,5 +68,5 @@ public interface NodeInformationProvider { * * @return a list of the stanza extensions defined in the node. */ - List getNodePacketExtensions(); + List getNodePacketExtensions(); } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/ServiceDiscoveryManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/ServiceDiscoveryManager.java index a009d260b..84b529aad 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/ServiceDiscoveryManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/ServiceDiscoveryManager.java @@ -1,6 +1,6 @@ /** * - * Copyright 2003-2007 Jive Software, 2018-2019 Florian Schmaus. + * Copyright 2003-2007 Jive Software, 2018-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. @@ -38,10 +38,10 @@ import org.jivesoftware.smack.XMPPConnectionRegistry; import org.jivesoftware.smack.XMPPException.XMPPErrorException; import org.jivesoftware.smack.iqrequest.AbstractIqRequestHandler; import org.jivesoftware.smack.iqrequest.IQRequestHandler.Mode; -import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.packet.StanzaError; +import org.jivesoftware.smack.util.CollectionUtil; import org.jivesoftware.smack.util.Objects; import org.jivesoftware.smack.util.StringUtils; @@ -88,7 +88,7 @@ public final class ServiceDiscoveryManager extends Manager { private static final Map instances = new WeakHashMap<>(); private final Set features = new HashSet<>(); - private DataForm extendedInfo = null; + private List extendedInfos = new ArrayList<>(2); private final Map nodeInformationProviders = new ConcurrentHashMap<>(); // Create a new ServiceDiscoveryManager on every established connection @@ -307,9 +307,8 @@ public final class ServiceDiscoveryManager extends Manager { for (String feature : getFeatures()) { response.addFeature(feature); } - if (extendedInfo != null) { - response.addExtension(extendedInfo); - } + + response.addExtensions(extendedInfos); } /** @@ -427,25 +426,59 @@ public final class ServiceDiscoveryManager extends Manager { * configure the extended info before logging to the server so that the * information is already available if it is required upon login. * - * @param info TODO javadoc me please - * the data form that contains the extend service discovery + * @param info the data form that contains the extend service discovery * information. + * @deprecated use {@link #addExtendedInfo(DataForm)} instead. */ + // TODO: Remove in Smack 4.5 + @Deprecated public synchronized void setExtendedInfo(DataForm info) { - extendedInfo = info; - // Notify others of a state change of SDM. In order to keep the state consistent, this - // method is synchronized - renewEntityCapsVersion(); + addExtendedInfo(info); } /** - * Returns the data form that is set as extended information for this Service Discovery instance (XEP-0128). + * Registers extended discovery information of this XMPP entity. When this + * client is queried for its information this data form will be returned as + * specified by XEP-0128. + *

* - * @see XEP-128: Service Discovery Extensions - * @return the data form + * Since no stanza is actually sent to the server it is safe to perform this + * operation before logging to the server. In fact, you may want to + * configure the extended info before logging to the server so that the + * information is already available if it is required upon login. + * + * @param extendedInfo the data form that contains the extend service discovery information. + * @return the old data form which got replaced (if any) + * @since 4.4.0 */ - public DataForm getExtendedInfo() { - return extendedInfo; + public DataForm addExtendedInfo(DataForm extendedInfo) { + String formType = extendedInfo.getFormType(); + StringUtils.requireNotNullNorEmpty(formType, "The data form must have a form type set"); + + DataForm removedDataForm; + synchronized (this) { + removedDataForm = DataForm.remove(extendedInfos, formType); + + extendedInfos.add(extendedInfo); + + // Notify others of a state change of SDM. In order to keep the state consistent, this + // method is synchronized + renewEntityCapsVersion(); + } + return removedDataForm; + } + + /** + * Remove the extended discovery information of the given form type. + * + * @param formType the type of the data form with the extended discovery information to remove. + * @since 4.4.0 + */ + public synchronized void removeExtendedInfo(String formType) { + DataForm removedForm = DataForm.remove(extendedInfos, formType); + if (removedForm != null) { + renewEntityCapsVersion(); + } } /** @@ -454,13 +487,21 @@ public final class ServiceDiscoveryManager extends Manager { * * @return the data form as List of PacketExtensions */ - public List getExtendedInfoAsList() { - List res = null; - if (extendedInfo != null) { - res = new ArrayList<>(1); - res.add(extendedInfo); - } - return res; + public synchronized List getExtendedInfo() { + return CollectionUtil.newListWith(extendedInfos); + } + + /** + * Returns the data form as List of PacketExtensions, or null if no data form is set. + * This representation is needed by some classes (e.g. EntityCapsManager, NodeInformationProvider) + * + * @return the data form as List of PacketExtensions + * @deprecated use {@link #getExtendedInfo()} instead. + */ + // TODO: Remove in Smack 4.5 + @Deprecated + public List getExtendedInfoAsList() { + return getExtendedInfo(); } /** @@ -471,10 +512,13 @@ public final class ServiceDiscoveryManager extends Manager { * operation before logging to the server. */ public synchronized void removeExtendedInfo() { - extendedInfo = null; - // Notify others of a state change of SDM. In order to keep the state consistent, this - // method is synchronized - renewEntityCapsVersion(); + int extendedInfosCount = extendedInfos.size(); + extendedInfos.clear(); + if (extendedInfosCount > 0) { + // Notify others of a state change of SDM. In order to keep the state consistent, this + // method is synchronized + renewEntityCapsVersion(); + } } /** diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/filetransfer/FaultTolerantNegotiator.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/filetransfer/FaultTolerantNegotiator.java deleted file mode 100644 index 5fc7bf6c3..000000000 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/filetransfer/FaultTolerantNegotiator.java +++ /dev/null @@ -1,111 +0,0 @@ -/** - * - * Copyright 2003-2006 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.filetransfer; - -import java.io.InputStream; -import java.io.OutputStream; - -import org.jivesoftware.smack.SmackException; -import org.jivesoftware.smack.XMPPConnection; -import org.jivesoftware.smack.XMPPException; -import org.jivesoftware.smack.XMPPException.XMPPErrorException; -import org.jivesoftware.smack.packet.IQ; -import org.jivesoftware.smack.packet.Stanza; - -import org.jivesoftware.smackx.bytestreams.ibb.packet.Open; -import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream; -import org.jivesoftware.smackx.si.packet.StreamInitiation; - -import org.jxmpp.jid.Jid; - - -/** - * The fault tolerant negotiator takes two stream negotiators, the primary and the secondary - * negotiator. If the primary negotiator fails during the stream negotiation process, the second - * negotiator is used. - */ -public class FaultTolerantNegotiator extends StreamNegotiator { - - private final StreamNegotiator primaryNegotiator; - private final StreamNegotiator secondaryNegotiator; - - public FaultTolerantNegotiator(XMPPConnection connection, StreamNegotiator primary, - StreamNegotiator secondary) { - super(connection); - this.primaryNegotiator = primary; - this.secondaryNegotiator = secondary; - } - - @Override - public void newStreamInitiation(Jid from, String streamID) { - primaryNegotiator.newStreamInitiation(from, streamID); - secondaryNegotiator.newStreamInitiation(from, streamID); - } - - @Override - InputStream negotiateIncomingStream(Stanza streamInitiation) { - throw new UnsupportedOperationException("Negotiation only handled by create incoming " + - "stream method."); - } - - @Override - public InputStream createIncomingStream(final StreamInitiation initiation) throws SmackException, XMPPErrorException, InterruptedException { - // This could be either an xep47 ibb 'open' iq or an xep65 streamhost iq - IQ initiationSet = initiateIncomingStream(connection(), initiation); - - StreamNegotiator streamNegotiator = determineNegotiator(initiationSet); - - return streamNegotiator.negotiateIncomingStream(initiationSet); - } - - private StreamNegotiator determineNegotiator(Stanza streamInitiation) { - if (streamInitiation instanceof Bytestream) { - return primaryNegotiator; - } else if (streamInitiation instanceof Open) { - return secondaryNegotiator; - } else { - throw new IllegalStateException("Unknown stream initiation type"); - } - } - - @Override - public OutputStream createOutgoingStream(String streamID, Jid initiator, Jid target) - throws SmackException, XMPPException, InterruptedException { - OutputStream stream; - try { - stream = primaryNegotiator.createOutgoingStream(streamID, initiator, target); - } - catch (Exception ex) { - stream = secondaryNegotiator.createOutgoingStream(streamID, initiator, target); - } - - return stream; - } - - @Override - public String[] getNamespaces() { - String[] primary = primaryNegotiator.getNamespaces(); - String[] secondary = secondaryNegotiator.getNamespaces(); - - String[] namespaces = new String[primary.length + secondary.length]; - System.arraycopy(primary, 0, namespaces, 0, primary.length); - System.arraycopy(secondary, 0, namespaces, primary.length, secondary.length); - - return namespaces; - } - -} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/filetransfer/FileTransferNegotiator.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/filetransfer/FileTransferNegotiator.java index cb5783206..9e670acb3 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/filetransfer/FileTransferNegotiator.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/filetransfer/FileTransferNegotiator.java @@ -42,6 +42,7 @@ import org.jivesoftware.smackx.filetransfer.FileTransferException.NoAcceptableTr import org.jivesoftware.smackx.filetransfer.FileTransferException.NoStreamMethodsOfferedException; import org.jivesoftware.smackx.si.packet.StreamInitiation; import org.jivesoftware.smackx.xdata.FormField; +import org.jivesoftware.smackx.xdata.ListSingleFormField; import org.jivesoftware.smackx.xdata.packet.DataForm; import org.jxmpp.jid.Jid; @@ -189,7 +190,7 @@ public final class FileTransferNegotiator extends Manager { public StreamNegotiator selectStreamNegotiator( FileTransferRequest request) throws NotConnectedException, NoStreamMethodsOfferedException, NoAcceptableTransferMechanisms, InterruptedException { StreamInitiation si = request.getStreamInitiation(); - FormField streamMethodField = getStreamMethodField(si + ListSingleFormField streamMethodField = getStreamMethodField(si .getFeatureNegotiationForm()); if (streamMethodField == null) { @@ -216,11 +217,11 @@ public final class FileTransferNegotiator extends Manager { return selectedStreamNegotiator; } - private static FormField getStreamMethodField(DataForm form) { - return form.getField(STREAM_DATA_FIELD_NAME); + private static ListSingleFormField getStreamMethodField(DataForm form) { + return (ListSingleFormField) form.getField(STREAM_DATA_FIELD_NAME); } - private StreamNegotiator getNegotiator(final FormField field) + private StreamNegotiator getNegotiator(final ListSingleFormField field) throws NoAcceptableTransferMechanisms { String variable; boolean isByteStream = false; @@ -239,12 +240,7 @@ public final class FileTransferNegotiator extends Manager { throw new FileTransferException.NoAcceptableTransferMechanisms(); } - if (isByteStream && isIBB) { - return new FaultTolerantNegotiator(connection(), - byteStreamTransferManager, - inbandTransferManager); - } - else if (isByteStream) { + if (isByteStream) { return byteStreamTransferManager; } else { @@ -355,11 +351,7 @@ public final class FileTransferNegotiator extends Manager { throw new FileTransferException.NoAcceptableTransferMechanisms(); } - if (isByteStream && isIBB) { - return new FaultTolerantNegotiator(connection(), - byteStreamTransferManager, inbandTransferManager); - } - else if (isByteStream) { + if (isByteStream) { return byteStreamTransferManager; } else { @@ -368,16 +360,15 @@ public final class FileTransferNegotiator extends Manager { } private static DataForm createDefaultInitiationForm() { - DataForm form = new DataForm(DataForm.Type.form); - FormField.Builder fieldBuilder = FormField.builder(); - fieldBuilder.setFieldName(STREAM_DATA_FIELD_NAME) - .setType(FormField.Type.list_single); + DataForm.Builder form = DataForm.builder() + .setType(DataForm.Type.form); + ListSingleFormField.Builder fieldBuilder = FormField.listSingleBuilder(STREAM_DATA_FIELD_NAME); if (!IBB_ONLY) { fieldBuilder.addOption(Bytestream.NAMESPACE); } fieldBuilder.addOption(DataPacketExtension.NAMESPACE); form.addField(fieldBuilder.build()); - return form; + return form.build(); } } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/filetransfer/IBBTransferNegotiator.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/filetransfer/IBBTransferNegotiator.java index ed7c328c5..2ed45ae59 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/filetransfer/IBBTransferNegotiator.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/filetransfer/IBBTransferNegotiator.java @@ -90,8 +90,8 @@ public class IBBTransferNegotiator extends StreamNegotiator { } @Override - public String[] getNamespaces() { - return new String[] { DataPacketExtension.NAMESPACE }; + public String getNamespace() { + return DataPacketExtension.NAMESPACE; } @Override diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/filetransfer/Socks5TransferNegotiator.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/filetransfer/Socks5TransferNegotiator.java index a29130469..516d7c564 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/filetransfer/Socks5TransferNegotiator.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/filetransfer/Socks5TransferNegotiator.java @@ -88,8 +88,8 @@ public class Socks5TransferNegotiator extends StreamNegotiator { } @Override - public String[] getNamespaces() { - return new String[] { Bytestream.NAMESPACE }; + public String getNamespace() { + return Bytestream.NAMESPACE; } @Override diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/filetransfer/StreamNegotiator.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/filetransfer/StreamNegotiator.java index 08031b6d3..e41d2a38d 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/filetransfer/StreamNegotiator.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/filetransfer/StreamNegotiator.java @@ -33,6 +33,7 @@ import org.jivesoftware.smack.util.EventManger.Callback; import org.jivesoftware.smackx.si.packet.StreamInitiation; import org.jivesoftware.smackx.xdata.FormField; +import org.jivesoftware.smackx.xdata.ListSingleFormField; import org.jivesoftware.smackx.xdata.packet.DataForm; import org.jxmpp.jid.Jid; @@ -69,33 +70,31 @@ public abstract class StreamNegotiator extends Manager { * initiator. * * @param streamInitiationOffer The offer from the stream initiator to connect for a stream. - * @param namespaces The namespace that relates to the accepted means of transfer. + * @param namespace The namespace that relates to the accepted means of transfer. * @return The response to be forwarded to the initiator. */ protected static StreamInitiation createInitiationAccept( - StreamInitiation streamInitiationOffer, String[] namespaces) { + StreamInitiation streamInitiationOffer, String namespace) { StreamInitiation response = new StreamInitiation(); response.setTo(streamInitiationOffer.getFrom()); response.setFrom(streamInitiationOffer.getTo()); response.setType(IQ.Type.result); response.setStanzaId(streamInitiationOffer.getStanzaId()); - DataForm form = new DataForm(DataForm.Type.submit); - FormField.Builder field = FormField.builder( + DataForm.Builder form = DataForm.builder(); + ListSingleFormField.Builder field = FormField.listSingleBuilder( FileTransferNegotiator.STREAM_DATA_FIELD_NAME); - for (String namespace : namespaces) { - field.addValue(namespace); - } + field.setValue(namespace); form.addField(field.build()); - response.setFeatureNegotiationForm(form); + response.setFeatureNegotiationForm(form.build()); return response; } protected final IQ initiateIncomingStream(final XMPPConnection connection, StreamInitiation initiation) throws NoResponseException, XMPPErrorException, NotConnectedException { final StreamInitiation response = createInitiationAccept(initiation, - getNamespaces()); + getNamespace()); newStreamInitiation(initiation.getFrom(), initiation.getSessionID()); @@ -182,7 +181,7 @@ public abstract class StreamNegotiator extends Manager { * @return Returns the XMPP namespace reserved for this particular type of * file transfer. */ - public abstract String[] getNamespaces(); + public abstract String getNamespace(); public static void signal(String eventKey, IQ eventValue) { initationSetEvents.signalEvent(eventKey, eventValue); diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/formtypes/FormFieldRegistry.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/formtypes/FormFieldRegistry.java new file mode 100644 index 000000000..4941d236d --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/formtypes/FormFieldRegistry.java @@ -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> REGISTRY = new HashMap<>(); + + private static final Map LOOKASIDE_REGISTRY = new HashMap<>(); + + private static final Map 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 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 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; + } + } +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/formtypes/package-info.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/formtypes/package-info.java new file mode 100644 index 000000000..4e6b2519a --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/formtypes/package-info.java @@ -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; diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/geoloc/GeoLocationManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/geoloc/GeoLocationManager.java index 3dbee6589..f29306251 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/geoloc/GeoLocationManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/geoloc/GeoLocationManager.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2017 Ishan Khanna, Fernando Ramirez 2019 Florian Schmaus + * Copyright 2015-2017 Ishan Khanna, Fernando Ramirez 2019-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. @@ -16,13 +16,9 @@ */ package org.jivesoftware.smackx.geoloc; -import java.util.List; import java.util.Map; -import java.util.Set; import java.util.WeakHashMap; -import java.util.concurrent.CopyOnWriteArraySet; -import org.jivesoftware.smack.AsyncButOrdered; import org.jivesoftware.smack.ConnectionCreationListener; import org.jivesoftware.smack.Manager; import org.jivesoftware.smack.SmackException.NoResponseException; @@ -30,31 +26,26 @@ import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPConnectionRegistry; import org.jivesoftware.smack.XMPPException.XMPPErrorException; -import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.Message; -import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; + import org.jivesoftware.smackx.geoloc.packet.GeoLocation; import org.jivesoftware.smackx.geoloc.provider.GeoLocationProvider; -import org.jivesoftware.smackx.pep.PepListener; +import org.jivesoftware.smackx.pep.PepEventListener; import org.jivesoftware.smackx.pep.PepManager; -import org.jivesoftware.smackx.pubsub.EventElement; -import org.jivesoftware.smackx.pubsub.ItemsExtension; import org.jivesoftware.smackx.pubsub.PayloadItem; import org.jivesoftware.smackx.pubsub.PubSubException.NotALeafNodeException; import org.jivesoftware.smackx.xdata.provider.FormFieldChildElementProviderManager; -import org.jxmpp.jid.BareJid; -import org.jxmpp.jid.EntityBareJid; import org.jxmpp.jid.Jid; /** * Entry point for Smacks API for XEP-0080: User Location. *
- * To publish a UserLocation, please use {@link #sendGeolocation(GeoLocation)} method. This will publish the node. + * To publish a UserLocation, please use {@link #publishGeoLocation(GeoLocation)} method. This will publish the node. *
* To stop publishing a UserLocation, please use {@link #stopPublishingGeolocation()} method. This will send a disble publishing signal. *
- * To add a {@link GeoLocationListener} in order to remain updated with other users GeoLocation, use {@link #addGeoLocationListener(GeoLocationListener)} method. + * To add a {@link PepEventListener} in order to remain updated with other users GeoLocation, use {@link #addGeoLocationListener(PepEventListener)} method. *
* To link a GeoLocation with {@link Message}, use `message.addExtension(geoLocation)`. *
@@ -65,16 +56,10 @@ import org.jxmpp.jid.Jid; */ public final class GeoLocationManager extends Manager { - public static final String GEOLOCATION_NODE = "http://jabber.org/protocol/geoloc"; - public static final String GEOLOCATION_NOTIFY = GEOLOCATION_NODE + "+notify"; + public static final String GEOLOCATION_NODE = GeoLocation.NAMESPACE; private static final Map INSTANCES = new WeakHashMap<>(); - private static boolean ENABLE_USER_LOCATION_NOTIFICATIONS_BY_DEFAULT = true; - - private final Set geoLocationListeners = new CopyOnWriteArraySet<>(); - private final AsyncButOrdered asyncButOrdered = new AsyncButOrdered(); - private final ServiceDiscoveryManager serviceDiscoveryManager; private final PepManager pepManager; static { @@ -108,31 +93,6 @@ public final class GeoLocationManager extends Manager { private GeoLocationManager(XMPPConnection connection) { super(connection); pepManager = PepManager.getInstanceFor(connection); - pepManager.addPepListener(new PepListener() { - - @Override - public void eventReceived(EntityBareJid from, EventElement event, Message message) { - if (!GEOLOCATION_NODE.equals(event.getEvent().getNode())) { - return; - } - - final BareJid contact = from.asBareJid(); - asyncButOrdered.performAsyncButOrdered(contact, () -> { - ItemsExtension itemsExtension = (ItemsExtension) event.getEvent(); - List items = itemsExtension.getExtensions(); - @SuppressWarnings("unchecked") - PayloadItem payload = (PayloadItem) items.get(0); - GeoLocation geoLocation = payload.getPayload(); - for (GeoLocationListener listener : geoLocationListeners) { - listener.onGeoLocationUpdated(contact, geoLocation, message); - } - }); - } - }); - serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection); - if (ENABLE_USER_LOCATION_NOTIFICATIONS_BY_DEFAULT) { - enableUserLocationNotifications(); - } } public void sendGeoLocationToJid(GeoLocation geoLocation, Jid jid) throws InterruptedException, @@ -160,18 +120,18 @@ public final class GeoLocationManager extends Manager { } /** - * Send geolocation through the PubSub node. + * Publish the user's geographic location through the Personal Eventing Protocol (PEP). * - * @param geoLocation TODO javadoc me please + * @param geoLocation the geographic location to publish. * @throws InterruptedException if the calling thread was interrupted. * @throws NotConnectedException if the XMPP connection is not connected. * @throws XMPPErrorException if there was an XMPP error returned. * @throws NoResponseException if there was no response from the remote entity. * @throws NotALeafNodeException if a PubSub leaf node operation was attempted on a non-leaf node. */ - public void sendGeolocation(GeoLocation geoLocation) + public void publishGeoLocation(GeoLocation geoLocation) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotALeafNodeException { - pepManager.publish(GeoLocation.NAMESPACE, new PayloadItem(geoLocation)); + pepManager.publish(GEOLOCATION_NODE, new PayloadItem(geoLocation)); } /** @@ -185,25 +145,14 @@ public final class GeoLocationManager extends Manager { */ public void stopPublishingGeolocation() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotALeafNodeException { - pepManager.publish(GeoLocation.NAMESPACE, new PayloadItem(GeoLocation.EMPTY_GEO_LOCATION)); + pepManager.publish(GEOLOCATION_NODE, new PayloadItem(GeoLocation.EMPTY_GEO_LOCATION)); } - public static void setGeoLocationNotificationsEnabledByDefault(boolean bool) { - ENABLE_USER_LOCATION_NOTIFICATIONS_BY_DEFAULT = bool; + public boolean addGeoLocationListener(PepEventListener listener) { + return pepManager.addPepEventListener(GEOLOCATION_NODE, GeoLocation.class, listener); } - public void enableUserLocationNotifications() { - serviceDiscoveryManager.addFeature(GEOLOCATION_NOTIFY); - } - - public void disableGeoLocationNotifications() { - serviceDiscoveryManager.removeFeature(GEOLOCATION_NOTIFY); - } - - public boolean addGeoLocationListener(GeoLocationListener geoLocationListener) { - return geoLocationListeners.add(geoLocationListener); - } - public boolean removeGeoLocationListener(GeoLocationListener geoLocationListener) { - return geoLocationListeners.remove(geoLocationListener); + public boolean removeGeoLocationListener(PepEventListener listener) { + return pepManager.removePepEventListener(listener); } } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/geoloc/packet/GeoLocation.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/geoloc/packet/GeoLocation.java index 3cf633ee8..79ba84de8 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/geoloc/packet/GeoLocation.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/geoloc/packet/GeoLocation.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2017 Ishan Khanna, Fernando Ramirez, 2019 Florian Schmaus + * Copyright 2015-2017 Ishan Khanna, Fernando Ramirez, 2019-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. @@ -19,14 +19,13 @@ package org.jivesoftware.smackx.geoloc.packet; import java.io.Serializable; import java.net.URI; import java.util.Date; -import java.util.logging.Level; -import java.util.logging.Logger; import javax.xml.namespace.QName; import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.Message; -import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smack.util.EqualsUtil; +import org.jivesoftware.smack.util.HashCode; import org.jivesoftware.smack.util.XmlStringBuilder; import org.jivesoftware.smackx.xdata.FormField; @@ -50,8 +49,6 @@ public final class GeoLocation implements Serializable, ExtensionElement, FormFi public static final GeoLocation EMPTY_GEO_LOCATION = GeoLocation.builder().build(); - private static final Logger LOGGER = Logger.getLogger(GeoLocation.class.getName()); - private final Double accuracy; private final Double alt; private final Double altAccuracy; @@ -77,50 +74,31 @@ public final class GeoLocation implements Serializable, ExtensionElement, FormFi private final String tzo; private final URI uri; - private GeoLocation(Double accuracy, Double alt, Double altAccuracy, String area, Double bearing, String building, String country, - String countryCode, String datum, String description, Double error, String floor, Double lat, - String locality, Double lon, String postalcode, String region, String room, Double speed, - String street, String text, Date timestamp, String tzo, URI uri) { - this.accuracy = accuracy; - this.alt = alt; - this.altAccuracy = altAccuracy; - this.area = area; - this.bearing = bearing; - this.building = building; - this.country = country; - this.countryCode = countryCode; - - // If datum is not included, receiver MUST assume WGS84; receivers MUST implement WGS84; senders MAY use another - // datum, but it is not recommended. - - if (StringUtils.isNullOrEmpty(datum)) { - datum = "WGS84"; - } - - this.datum = datum; - this.description = description; - - // error element is deprecated in favor of accuracy - if (accuracy != null) { - error = null; - LOGGER.log(Level.WARNING, - "Error and accuracy set. Ignoring error as it is deprecated in favor of accuracy"); - } - - this.error = error; - this.floor = floor; - this.lat = lat; - this.locality = locality; - this.lon = lon; - this.postalcode = postalcode; - this.region = region; - this.room = room; - this.speed = speed; - this.street = street; - this.text = text; - this.timestamp = timestamp; - this.tzo = tzo; - this.uri = uri; + private GeoLocation(Builder builder) { + accuracy = builder.accuracy; + alt = builder.alt; + altAccuracy = builder.altAccuracy; + area = builder.area; + bearing = builder.bearing; + building = builder.building; + country = builder.country; + countryCode = builder.countryCode; + datum = builder.datum; + description = builder.description; + error = builder.error; + floor = builder.floor; + lat = builder.lat; + locality = builder.locality; + lon = builder.lon; + postalcode = builder.postalcode; + region = builder.region; + room = builder.room; + speed = builder.speed; + street = builder.street; + text = builder.text; + timestamp = builder.timestamp; + tzo = builder.tzo; + uri = builder.uri; } public Double getAccuracy() { @@ -163,6 +141,13 @@ public final class GeoLocation implements Serializable, ExtensionElement, FormFi return description; } + /** + * Get the error. + * + * @return the error. + * @deprecated use {@link #getAccuracy()} instead. + */ + @Deprecated public Double getError() { return error; } @@ -266,6 +251,70 @@ public final class GeoLocation implements Serializable, ExtensionElement, FormFi return NAMESPACE; } + private final HashCode.Cache hashCodeCache = new HashCode.Cache(); + + @Override + public int hashCode() { + return hashCodeCache.getHashCode(c -> + c + .append(accuracy) + .append(alt) + .append(altAccuracy) + .append(area) + .append(bearing) + .append(building) + .append(country) + .append(countryCode) + .append(datum) + .append(description) + .append(error) + .append(floor) + .append(lat) + .append(locality) + .append(lon) + .append(postalcode) + .append(region) + .append(room) + .append(speed) + .append(street) + .append(text) + .append(timestamp) + .append(tzo) + .append(uri) + ); + } + + @Override + public boolean equals(Object obj) { + return EqualsUtil.equals(this, obj, (e, o) -> { + e + .append(accuracy, o.accuracy) + .append(altAccuracy, o.altAccuracy) + .append(area, o.area) + .append(bearing, o.bearing) + .append(building, o.building) + .append(country, o.country) + .append(countryCode, o.countryCode) + .append(datum, o.datum) + .append(description, o.description) + .append(error, o.error) + .append(floor, o.floor) + .append(lat, o.lat) + .append(locality, o.locality) + .append(lon, o.lon) + .append(postalcode, o.postalcode) + .append(region, o.region) + .append(room, o.room) + .append(speed, o.speed) + .append(street, o.street) + .append(text, o.text) + .append(timestamp, o.timestamp) + .append(tzo, o.tzo) + .append(uri, o.uri) + ; + }); + } + /** * Returns a new instance of {@link Builder}. * @return Builder @@ -318,7 +367,11 @@ public final class GeoLocation implements Serializable, ExtensionElement, FormFi private String building; private String country; private String countryCode; - private String datum; + + // If datum is not included, receiver MUST assume WGS84; receivers MUST implement WGS84; senders MAY use another + // datum, but it is not recommended. + private String datum = "WGS84"; + private String description; private Double error; private String floor; @@ -453,7 +506,9 @@ public final class GeoLocation implements Serializable, ExtensionElement, FormFi * * @param error error in arc minutes * @return Builder + * @deprecated use {@link #setAccuracy(Double)} instead. */ + @Deprecated public Builder setError(Double error) { this.error = error; return this; @@ -610,10 +665,7 @@ public final class GeoLocation implements Serializable, ExtensionElement, FormFi * @return GeoLocation */ public GeoLocation build() { - - return new GeoLocation(accuracy, alt, altAccuracy, area, bearing, building, country, countryCode, datum, description, - error, floor, lat, locality, lon, postalcode, region, room, speed, street, text, timestamp, - tzo, uri); + return new GeoLocation(this); } } } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/geoloc/provider/GeoLocationProvider.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/geoloc/provider/GeoLocationProvider.java index cf6234aa4..08275293e 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/geoloc/provider/GeoLocationProvider.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/geoloc/provider/GeoLocationProvider.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2017 Ishan Khanna, Fernando Ramirez, 2019 Florian Schmaus + * Copyright 2015-2017 Ishan Khanna, Fernando Ramirez, 2019-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. @@ -79,7 +79,7 @@ public class GeoLocationProvider extends ExtensionElementProvider { builder.setDescription(parser.nextText()); break; case "error": - builder.setError(ParserUtils.getDoubleFromNextText(parser)); + parseError(builder, parser); break; case "floor": builder.setFloor(parser.nextText()); @@ -136,6 +136,12 @@ public class GeoLocationProvider extends ExtensionElementProvider { return builder.build(); } + @SuppressWarnings("deprecation") + private static void parseError(GeoLocation.Builder builder, XmlPullParser parser) throws XmlPullParserException, IOException { + double error = ParserUtils.getDoubleFromNextText(parser); + builder.setError(error); + } + public static class GeoLocationFormFieldChildElementProvider extends FormFieldChildElementProvider { public static final GeoLocationFormFieldChildElementProvider INSTANCE = new GeoLocationFormFieldChildElementProvider(); diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/mood/MoodListener.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/mood/MoodListener.java index cc644bd07..145aa0b41 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/mood/MoodListener.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/mood/MoodListener.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018 Paul Schaub. + * Copyright 2018 Paul Schaub, 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. @@ -20,9 +20,10 @@ import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smackx.mood.element.MoodElement; -import org.jxmpp.jid.BareJid; +import org.jxmpp.jid.EntityBareJid; public interface MoodListener { - void onMoodUpdated(BareJid jid, Message message, MoodElement moodElement); + void onMoodUpdated(EntityBareJid from, MoodElement moodElement, String id, Message message); + } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/mood/MoodManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/mood/MoodManager.java index 27f270ae8..5261a45e8 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/mood/MoodManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/mood/MoodManager.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018 Paul Schaub. + * Copyright 2018 Paul Schaub, 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. @@ -16,12 +16,9 @@ */ package org.jivesoftware.smackx.mood; -import java.util.HashSet; import java.util.Map; -import java.util.Set; import java.util.WeakHashMap; -import org.jivesoftware.smack.AsyncButOrdered; import org.jivesoftware.smack.Manager; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.XMPPConnection; @@ -29,21 +26,13 @@ import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.provider.ProviderManager; -import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; import org.jivesoftware.smackx.mood.element.MoodConcretisation; import org.jivesoftware.smackx.mood.element.MoodElement; import org.jivesoftware.smackx.mood.provider.MoodConcretisationProvider; -import org.jivesoftware.smackx.pep.PepListener; +import org.jivesoftware.smackx.pep.PepEventListener; import org.jivesoftware.smackx.pep.PepManager; -import org.jivesoftware.smackx.pubsub.EventElement; -import org.jivesoftware.smackx.pubsub.ItemsExtension; -import org.jivesoftware.smackx.pubsub.LeafNode; import org.jivesoftware.smackx.pubsub.PayloadItem; import org.jivesoftware.smackx.pubsub.PubSubException; -import org.jivesoftware.smackx.pubsub.PubSubManager; - -import org.jxmpp.jid.BareJid; -import org.jxmpp.jid.EntityBareJid; /** * Entry point for Smacks API for XEP-0107: User Mood. @@ -51,8 +40,8 @@ import org.jxmpp.jid.EntityBareJid; * To set a mood, please use one of the {@link #setMood(Mood)} methods. This will publish the users mood to a pubsub * node.
*
- * In order to get updated about other users moods, register a {@link MoodListener} at - * {@link #addMoodListener(MoodListener)}. That listener will get updated about any incoming mood updates of contacts.
+ * In order to get updated about other users moods, register a {@link PepEventListener} at + * {@link #addMoodListener(PepEventListener)}. That listener will get updated about any incoming mood updates of contacts.
*
* To stop publishing the users mood, refer to {@link #clearMood()}.
*
@@ -68,39 +57,15 @@ import org.jxmpp.jid.EntityBareJid; public final class MoodManager extends Manager { public static final String MOOD_NODE = "http://jabber.org/protocol/mood"; - public static final String MOOD_NOTIFY = MOOD_NODE + "+notify"; private static final Map INSTANCES = new WeakHashMap<>(); - private final Set moodListeners = new HashSet<>(); - private final AsyncButOrdered asyncButOrdered = new AsyncButOrdered<>(); - private PubSubManager pubSubManager; + private final PepManager pepManager; private MoodManager(XMPPConnection connection) { super(connection); - ServiceDiscoveryManager.getInstanceFor(connection).addFeature(MOOD_NOTIFY); - PepManager.getInstanceFor(connection).addPepListener(new PepListener() { - @Override - public void eventReceived(final EntityBareJid from, final EventElement event, final Message message) { - if (!MOOD_NODE.equals(event.getEvent().getNode())) { - return; - } - final BareJid contact = from.asBareJid(); - asyncButOrdered.performAsyncButOrdered(contact, new Runnable() { - @Override - public void run() { - ItemsExtension items = (ItemsExtension) event.getExtensions().get(0); - PayloadItem payload = (PayloadItem) items.getItems().get(0); - MoodElement mood = (MoodElement) payload.getPayload(); - - for (MoodListener listener : moodListeners) { - listener.onMoodUpdated(contact, message, mood); - } - } - }); - } - }); + pepManager = PepManager.getInstanceFor(connection); } public static synchronized MoodManager getInstanceFor(XMPPConnection connection) { @@ -147,12 +112,7 @@ public final class MoodManager extends Manager { private void publishMood(MoodElement moodElement) throws SmackException.NotLoggedInException, InterruptedException, PubSubException.NotALeafNodeException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException { - if (pubSubManager == null) { - pubSubManager = PubSubManager.getInstanceFor(getAuthenticatedConnectionOrThrow(), connection().getUser().asBareJid()); - } - - LeafNode node = pubSubManager.getOrCreateLeafNode(MOOD_NODE); - node.publish(new PayloadItem<>(moodElement)); + pepManager.publish(MOOD_NODE, new PayloadItem<>(moodElement)); } private static MoodElement buildMood(Mood mood, MoodConcretisation concretisation, String text) { @@ -170,11 +130,11 @@ public final class MoodManager extends Manager { message.addExtension(element); } - public synchronized void addMoodListener(MoodListener listener) { - moodListeners.add(listener); + public boolean addMoodListener(PepEventListener listener) { + return pepManager.addPepEventListener(MOOD_NODE, MoodElement.class, listener); } - public synchronized void removeMoodListener(MoodListener listener) { - moodListeners.remove(listener); + public boolean removeMoodListener(PepEventListener listener) { + return pepManager.removePepEventListener(listener); } } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MucConfigFormManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MucConfigFormManager.java index 5a7adcc64..8d941ee06 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MucConfigFormManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MucConfigFormManager.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015 Florian Schmaus + * Copyright 2015-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. @@ -23,17 +23,18 @@ import java.util.List; import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.XMPPException.XMPPErrorException; -import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smackx.muc.MultiUserChatException.MucConfigurationNotSupportedException; -import org.jivesoftware.smackx.xdata.Form; 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.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. *

* Room configuration needs either be done right after the room is created and still locked. Or at @@ -43,12 +44,17 @@ import org.jxmpp.jid.util.JidUtil; *

*

* The manager may not provide all possible configuration options. If you want direct access to the - * configuraiton form, use {@link MultiUserChat#getConfigurationForm()} and - * {@link MultiUserChat#sendConfigurationForm(Form)}. + * configuration form, use {@link MultiUserChat#getConfigurationForm()} and + * {@link MultiUserChat#sendConfigurationForm(FillableForm)}. *

*/ public class MucConfigFormManager { - /** + + private static final String HASH_ROOMCONFIG = "#roomconfig"; + + public static final String FORM_TYPE = MultiUserChatConstants.NAMESPACE + HASH_ROOMCONFIG; + + /** * The constant String {@value}. * * @see XEP-0045 § 10. Owner Use Cases @@ -73,7 +79,7 @@ public class MucConfigFormManager { public static final String MUC_ROOMCONFIG_ROOMSECRET = "muc#roomconfig_roomsecret"; private final MultiUserChat multiUserChat; - private final Form answerForm; + private final FillableForm answerForm; private final List owners; /** @@ -94,20 +100,13 @@ public class MucConfigFormManager { // Set the answer form Form configForm = multiUserChat.getConfigurationForm(); - this.answerForm = configForm.createAnswerForm(); - // 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()); - } + this.answerForm = configForm.getFillableForm(); // 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 - List ownerStrings = answerForm.getField(MUC_ROOMCONFIG_ROOMOWNERS).getValues(); + List ownerStrings = roomOwnersFormField.getValues(); owners = new ArrayList<>(ownerStrings.size()); 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 XMPPErrorException if there was an XMPP error returned. diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MucEnterConfiguration.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MucEnterConfiguration.java index 51b79fb57..de2b6606e 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MucEnterConfiguration.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MucEnterConfiguration.java @@ -133,7 +133,7 @@ public final class MucEnterConfiguration { * * @param presenceBuilderConsumer a consumer which will be passed the presence build. * @return a reference to this builder. - * @since 4.5 + * @since 4.4.0 */ public Builder withPresence(Consumer presenceBuilderConsumer) { presenceBuilderConsumer.accept(joinPresenceBuilder); diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChat.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChat.java index 747b1469f..422320bf1 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChat.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChat.java @@ -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"); * 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.MUCUser; 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.TextSingleFormField; +import org.jivesoftware.smackx.xdata.form.FillableForm; +import org.jivesoftware.smackx.xdata.form.Form; import org.jivesoftware.smackx.xdata.packet.DataForm; import org.jxmpp.jid.DomainBareJid; @@ -149,7 +151,6 @@ public class MultiUserChat { private String subject; private EntityFullJid myRoomJid; - private boolean joined = false; private StanzaCollector messageCollector; MultiUserChat(XMPPConnection connection, EntityBareJid room, MultiUserChatManager multiUserChatManager) { @@ -233,10 +234,13 @@ public class MultiUserChat { occupantsMap.remove(from); MUCUser mucUser = MUCUser.from(packet); if (mucUser != null && mucUser.hasStatus()) { + if (isUserStatusModification) { + userHasLeft(); + } // Fire events according to the received presence code checkPresenceCode( mucUser.getStatus(), - presence.getFrom().equals(myRoomJID), + isUserStatusModification, mucUser, from); } else { @@ -246,6 +250,27 @@ public class MultiUserChat { listener.left(from); } } + + Destroy destroy = mucUser.getDestroy(); + // The room has been destroyed. + if (destroy != null) { + EntityBareJid alternateMucJid = destroy.getJid(); + final MultiUserChat alternateMuc; + if (alternateMucJid == null) { + alternateMuc = null; + } else { + alternateMuc = multiUserChatManager.getMultiUserChat(alternateMucJid); + } + + for (UserStatusListener listener : userStatusListeners) { + listener.roomDestroyed(alternateMuc, destroy.getReason()); + } + } + } + if (isUserStatusModification) { + for (UserStatusListener listener : userStatusListeners) { + listener.removed(mucUser, presence); + } } break; default: @@ -383,8 +408,6 @@ public class MultiUserChat { Resourcepart receivedNickname = presence.getFrom().getResourceOrThrow(); setNickname(receivedNickname); - joined = true; - // Update the list of joined rooms multiUserChatManager.addJoinedRoom(room); return presence; @@ -436,7 +459,7 @@ public class MultiUserChat { public synchronized MucCreateConfigFormHandle create(Resourcepart nickname) throws NoResponseException, XMPPErrorException, InterruptedException, MucAlreadyJoinedException, NotConnectedException, MissingMucCreationAcknowledgeException, NotAMucServiceException { - if (joined) { + if (isJoined()) { throw new MucAlreadyJoinedException(); } @@ -491,7 +514,7 @@ public class MultiUserChat { */ public synchronized MucCreateConfigFormHandle createOrJoin(MucEnterConfiguration mucEnterConfiguration) throws NoResponseException, XMPPErrorException, InterruptedException, MucAlreadyJoinedException, NotConnectedException, NotAMucServiceException { - if (joined) { + if (isJoined()) { throw new MucAlreadyJoinedException(); } @@ -512,8 +535,8 @@ public class MultiUserChat { * instant room, use {@link #makeInstant()}. *

* 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 MultiUserChat#sendConfigurationForm(Form)}. + * {@link Form#getFillableForm()}, fill it out and send it back to the room with + * {@link MultiUserChat#sendConfigurationForm(FillableForm)}. *

*/ public class MucCreateConfigFormHandle { @@ -531,7 +554,7 @@ public class MultiUserChat { */ public void makeInstant() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { - sendConfigurationForm(new Form(DataForm.Type.submit)); + sendConfigurationForm(null); } /** @@ -662,7 +685,7 @@ public class MultiUserChat { throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException, NotAMucServiceException { // If we've already joined the room, leave it before joining under a new // nickname. - if (joined) { + if (isJoined()) { try { leaveSync(); } @@ -680,7 +703,7 @@ public class MultiUserChat { * @return true if currently in the multi user chat room. */ public boolean isJoined() { - return joined; + return myRoomJid != null; } /** @@ -716,10 +739,6 @@ public class MultiUserChat { // "if (!joined) return" because it should be always be possible to leave the room in case the instance's // state does not reflect the actual state. - // Reset occupant information first so that we are assume that we left the room even if sendStanza() would - // throw. - userHasLeft(); - final EntityFullJid myRoomJid = this.myRoomJid; if (myRoomJid == null) { throw new MucNotJoinedException(this); @@ -741,6 +760,10 @@ public class MultiUserChat { ) ); + // Reset occupant information first so that we are assume that we left the room even if sendStanza() would + // throw. + userHasLeft(); + Presence reflectedLeavePresence = connection.createStanzaCollectorAndSend(reflectedLeavePresenceFilter, leavePresence).nextResultOrThrow(); return reflectedLeavePresence; @@ -783,7 +806,8 @@ public class MultiUserChat { iq.setType(IQ.Type.get); IQ answer = connection.createStanzaCollectorAndSend(iq).nextResultOrThrow(); - return Form.getFormFrom(answer); + DataForm dataForm = DataForm.from(answer, MucConfigFormManager.FORM_TYPE); + return new Form(dataForm); } /** @@ -796,11 +820,19 @@ public class MultiUserChat { * @throws NotConnectedException if the XMPP connection is not connected. * @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(); iq.setTo(room); iq.setType(IQ.Type.set); - iq.addExtension(form.getDataFormToSend()); + iq.addExtension(dataForm); connection.createStanzaCollectorAndSend(iq).nextResultOrThrow(); } @@ -828,7 +860,8 @@ public class MultiUserChat { reg.setTo(room); IQ result = connection.createStanzaCollectorAndSend(reg).nextResultOrThrow(); - return Form.getFormFrom(result); + DataForm dataForm = DataForm.from(result); + return new Form(dataForm); } /** @@ -848,11 +881,11 @@ public class MultiUserChat { * @throws NotConnectedException if the XMPP connection is not connected. * @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(); reg.setType(IQ.Type.set); reg.setTo(room); - reg.addExtension(form.getDataFormToSend()); + reg.addExtension(form.getDataFormToSubmit()); connection.createStanzaCollectorAndSend(reg).nextResultOrThrow(); } @@ -1135,7 +1168,7 @@ public class MultiUserChat { Objects.requireNonNull(nickname, "Nickname must not be null or blank."); // Check that we already have joined the room before attempting to change the // nickname. - if (!joined) { + if (!isJoined()) { throw new MucNotJoinedException(this); } final EntityFullJid jid = JidCreate.entityFullFrom(room, nickname); @@ -1178,11 +1211,6 @@ public class MultiUserChat { throw new MucNotJoinedException(this); } - // Check that we already have joined the room before attempting to change the - // availability status. - if (!joined) { - throw new MucNotJoinedException(this); - } // We change the availability status by sending a presence packet to the room with the // new presence status and mode Presence joinPresence = connection.getStanzaFactory().buildPresenceStanza() @@ -1231,19 +1259,17 @@ public class MultiUserChat { * @since 4.1 */ public void requestVoice() throws NotConnectedException, InterruptedException { - DataForm form = new DataForm(DataForm.Type.submit); - FormField.Builder formTypeField = FormField.builder(FormField.FORM_TYPE); - formTypeField.addValue(MUCInitialPresence.NAMESPACE + "#request"); - form.addField(formTypeField.build()); - FormField.Builder requestVoiceField = FormField.builder("muc#role"); - requestVoiceField.setType(FormField.Type.text_single); + DataForm.Builder form = DataForm.builder() + .setFormType(MUCInitialPresence.NAMESPACE + "#request"); + + TextSingleFormField.Builder requestVoiceField = FormField.textSingleBuilder("muc#role"); requestVoiceField.setLabel("Requested role"); - requestVoiceField.addValue("participant"); + requestVoiceField.setValue("participant"); form.addField(requestVoiceField.build()); Message message = connection.getStanzaFactory().buildMessageStanza() .to(room) - .addExtension(form) + .addExtension(form.build()) .build(); connection.sendStanza(message); } @@ -2119,7 +2145,7 @@ public class MultiUserChat { // to call leave() in order to resync the state. And leave() requires the nickname to send the unsubscribe // presence. occupantsMap.clear(); - joined = false; + myRoomJid = null; // Update the list of joined rooms multiUserChatManager.removeJoinedRoom(room); removeConnectionCallbacks(); @@ -2437,9 +2463,6 @@ public class MultiUserChat { if (statusCodes.contains(Status.KICKED_307)) { // Check if this occupant was kicked if (isUserModification) { - // Reset occupant information. - userHasLeft(); - for (UserStatusListener listener : userStatusListeners) { listener.kicked(mucUser.getItem().getActor(), mucUser.getItem().getReason()); } @@ -2454,15 +2477,9 @@ public class MultiUserChat { if (statusCodes.contains(Status.BANNED_301)) { // Check if this occupant was banned if (isUserModification) { - joined = false; for (UserStatusListener listener : userStatusListeners) { listener.banned(mucUser.getItem().getActor(), mucUser.getItem().getReason()); } - - // Reset occupant information. - occupantsMap.clear(); - myRoomJid = null; - userHasLeft(); } else { for (ParticipantStatusListener listener : participantStatusListeners) { @@ -2474,15 +2491,9 @@ public class MultiUserChat { if (statusCodes.contains(Status.REMOVED_AFFIL_CHANGE_321)) { // Check if this occupant's membership was revoked if (isUserModification) { - joined = false; for (UserStatusListener listener : userStatusListeners) { listener.membershipRevoked(); } - - // Reset occupant information. - occupantsMap.clear(); - myRoomJid = null; - userHasLeft(); } } // A occupant has changed his nickname in the room @@ -2491,18 +2502,6 @@ public class MultiUserChat { listener.nicknameChanged(from, mucUser.getItem().getNick()); } } - // The room has been destroyed. - if (mucUser.getDestroy() != null) { - MultiUserChat alternateMUC = multiUserChatManager.getMultiUserChat(mucUser.getDestroy().getJid()); - for (UserStatusListener listener : userStatusListeners) { - listener.roomDestroyed(alternateMUC, mucUser.getDestroy().getReason()); - } - - // Reset occupant information. - occupantsMap.clear(); - myRoomJid = null; - userHasLeft(); - } } /** diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/geoloc/GeoLocationListener.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatConstants.java similarity index 63% rename from smack-extensions/src/main/java/org/jivesoftware/smackx/geoloc/GeoLocationListener.java rename to smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatConstants.java index 8b1259ec7..ffc00a5a9 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/geoloc/GeoLocationListener.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatConstants.java @@ -1,6 +1,6 @@ /** * - * Copyright 2020 Aditya Borikar. + * 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. @@ -14,13 +14,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.jivesoftware.smackx.geoloc; +package org.jivesoftware.smackx.muc; -import org.jivesoftware.smack.packet.Message; -import org.jivesoftware.smackx.geoloc.packet.GeoLocation; +public class MultiUserChatConstants { -import org.jxmpp.jid.BareJid; + public static final String NAMESPACE = "http://jabber.org/protocol/muc"; -public interface GeoLocationListener { - void onGeoLocationUpdated(BareJid jid, GeoLocation geoLocation, Message message); } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/RoomInfo.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/RoomInfo.java index 5c24b578e..d321903dd 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/RoomInfo.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/RoomInfo.java @@ -25,8 +25,8 @@ import java.util.logging.Level; import java.util.logging.Logger; import org.jivesoftware.smackx.disco.packet.DiscoverInfo; -import org.jivesoftware.smackx.xdata.Form; import org.jivesoftware.smackx.xdata.FormField; +import org.jivesoftware.smackx.xdata.packet.DataForm; import org.jxmpp.jid.EntityBareJid; import org.jxmpp.jid.Jid; @@ -130,7 +130,7 @@ public class RoomInfo { /** * The rooms extended configuration form; */ - private final Form form; + private final DataForm form; RoomInfo(DiscoverInfo info) { final Jid from = info.getFrom(); @@ -166,7 +166,7 @@ public class RoomInfo { URL logs = null; String pubsub = null; // Get the information based on the discovered extended information - form = Form.getFormFrom(info); + form = DataForm.from(info); if (form != null) { FormField descField = form.getField("muc#roominfo_description"); if (descField != null && !descField.getValues().isEmpty()) { @@ -191,7 +191,7 @@ public class RoomInfo { FormField contactJidField = form.getField("muc#roominfo_contactjid"); if (contactJidField != null && !contactJidField.getValues().isEmpty()) { - List contactJidValues = contactJidField.getValues(); + List contactJidValues = contactJidField.getValues(); 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: * Multi User Chat - 6.5 Querying for Room Information */ - public Form getForm() { + public DataForm getForm() { return form; } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/UserStatusListener.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/UserStatusListener.java index 105a09a0d..5e8614ed6 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/UserStatusListener.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/UserStatusListener.java @@ -17,11 +17,21 @@ package org.jivesoftware.smackx.muc; +import org.jivesoftware.smack.packet.Presence; + +import org.jivesoftware.smackx.muc.packet.MUCUser; + import org.jxmpp.jid.Jid; /** - * A listener that is fired anytime your participant's status in a room is changed, such as the - * user being kicked, banned, or granted admin permissions or the room is destroyed. + * A listener that is fired anytime your participant's status in a room is changed, such as the user being kicked, + * banned, or granted admin permissions or the room is destroyed. + *

+ * Note that the methods {@link #kicked(Jid, String)}, {@link #banned(Jid, String)} and + * {@link #roomDestroyed(MultiUserChat, String)} will be called before the generic {@link #removed(MUCUser, Presence)} + * callback will be invoked. The generic {@link #removed(MUCUser, Presence)} callback will be invoked every time the user + * was removed from the MUC involuntarily. It is hence the recommended callback to listen for and act upon. + *

* * @author Gaston Dombiak */ @@ -33,6 +43,7 @@ public interface UserStatusListener { * * @param actor the moderator that kicked your user from the room (e.g. user@host.org). * @param reason the reason provided by the actor to kick you from the room. + * @see #removed(MUCUser, Presence) */ void kicked(Jid actor, String reason); @@ -58,10 +69,21 @@ public interface UserStatusListener { * * @param actor the administrator that banned your user (e.g. user@host.org). * @param reason the reason provided by the administrator to banned you. + * @see #removed(MUCUser, Presence) */ void banned(Jid actor, String reason); - /** + /** + * Called when a user is involuntarily removed from the room. + * + * @param mucUser the optional muc#user extension element + * @param presence the carrier presence + * @since 4.4.0 + */ + default void removed(MUCUser mucUser, Presence presence) { + }; + + /** * Called when an administrator grants your user membership to the room. This means that you * will be able to join the members-only room. * @@ -128,6 +150,7 @@ public interface UserStatusListener { * * @param alternateMUC an alternate MultiUserChat, may be null. * @param reason the reason why the room was closed, may be null. + * @see #removed(MUCUser, Presence) */ void roomDestroyed(MultiUserChat alternateMUC, String reason); diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/packet/MUCUser.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/packet/MUCUser.java index 87cd64ace..81eda1319 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/packet/MUCUser.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/packet/MUCUser.java @@ -410,6 +410,7 @@ public class MUCUser implements ExtensionElement { public static final Status NEW_NICKNAME_303 = Status.create(303); public static final Status KICKED_307 = Status.create(307); public static final Status REMOVED_AFFIL_CHANGE_321 = Status.create(321); + public static final Status REMOVED_FOR_TECHNICAL_REASONS_333 = Status.create(333); private final Integer code; @@ -419,10 +420,14 @@ public class MUCUser implements ExtensionElement { } public static Status create(Integer i) { - Status status = statusMap.get(i); - if (status == null) { - status = new Status(i); - statusMap.put(i, status); + Status status; + // TODO: Use computeIfAbsent once Smack's minimum required Android SDK level is 24 or higher. + synchronized (statusMap) { + status = statusMap.get(i); + if (status == null) { + status = new Status(i); + statusMap.put(i, status); + } } return status; } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/offline/OfflineMessageManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/offline/OfflineMessageManager.java index 4756c890d..8021054da 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/offline/OfflineMessageManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/offline/OfflineMessageManager.java @@ -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"); * 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.offline.packet.OfflineMessageInfo; 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 @@ -115,12 +115,12 @@ public final class OfflineMessageManager extends Manager { */ public int getMessageCount() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { DiscoverInfo info = serviceDiscoveryManager.discoverInfo(null, namespace); - Form extendedInfo = Form.getFormFrom(info); - if (extendedInfo != null) { - String value = extendedInfo.getField("number_of_messages").getFirstValue(); - return Integer.parseInt(value); + DataForm dataForm = DataForm.from(info, namespace); + if (dataForm == null) { + return 0; } - return 0; + String numberOfMessagesString = dataForm.getField("number_of_messages").getFirstValue(); + return Integer.parseInt(numberOfMessagesString); } /** diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/usertune/UserTuneListener.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pep/PepEventListener.java similarity index 66% rename from smack-extensions/src/main/java/org/jivesoftware/smackx/usertune/UserTuneListener.java rename to smack-extensions/src/main/java/org/jivesoftware/smackx/pep/PepEventListener.java index e020e4ec2..6abf2e77f 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/usertune/UserTuneListener.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pep/PepEventListener.java @@ -1,6 +1,6 @@ /** * - * Copyright 2019 Aditya Borikar. + * 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. @@ -14,15 +14,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.jivesoftware.smackx.usertune; +package org.jivesoftware.smackx.pep; +import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.Message; -import org.jivesoftware.smackx.usertune.element.UserTuneElement; +import org.jxmpp.jid.EntityBareJid; -import org.jxmpp.jid.BareJid; +public interface PepEventListener { -public interface UserTuneListener { + void onPepEvent(EntityBareJid from, E event, String id, Message carrierMessage); - void onUserTuneUpdated(BareJid jid, Message message, UserTuneElement userTuneElement); } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pep/PepManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pep/PepManager.java index b31a3d3e2..20b009287 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pep/PepManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pep/PepManager.java @@ -1,6 +1,6 @@ /** * - * Copyright 2003-2007 Jive Software, 2015-2019 Florian Schmaus + * Copyright 2003-2007 Jive Software, 2015-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. @@ -17,10 +17,13 @@ package org.jivesoftware.smackx.pep; +import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.CopyOnWriteArraySet; +import java.util.logging.Logger; import org.jivesoftware.smack.AsyncButOrdered; import org.jivesoftware.smack.Manager; @@ -30,20 +33,26 @@ import org.jivesoftware.smack.StanzaListener; import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPException.XMPPErrorException; import org.jivesoftware.smack.filter.AndFilter; +import org.jivesoftware.smack.filter.MessageTypeFilter; import org.jivesoftware.smack.filter.StanzaFilter; -import org.jivesoftware.smack.filter.jidtype.AbstractJidTypeFilter.JidType; import org.jivesoftware.smack.filter.jidtype.FromJidTypeFilter; +import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.NamedElement; import org.jivesoftware.smack.packet.Stanza; +import org.jivesoftware.smack.util.CollectionUtil; +import org.jivesoftware.smack.util.MultiMap; import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; import org.jivesoftware.smackx.pubsub.EventElement; import org.jivesoftware.smackx.pubsub.Item; +import org.jivesoftware.smackx.pubsub.ItemsExtension; import org.jivesoftware.smackx.pubsub.LeafNode; +import org.jivesoftware.smackx.pubsub.PayloadItem; import org.jivesoftware.smackx.pubsub.PubSubException.NotALeafNodeException; import org.jivesoftware.smackx.pubsub.PubSubFeature; import org.jivesoftware.smackx.pubsub.PubSubManager; -import org.jivesoftware.smackx.pubsub.filter.EventExtensionFilter; +import org.jivesoftware.smackx.pubsub.filter.EventItemsExtensionFilter; import org.jxmpp.jid.BareJid; import org.jxmpp.jid.EntityBareJid; @@ -70,6 +79,8 @@ import org.jxmpp.jid.EntityBareJid; */ public final class PepManager extends Manager { + private static final Logger LOGGER = Logger.getLogger(PepManager.class.getName()); + private static final Map INSTANCES = new WeakHashMap<>(); public static synchronized PepManager getInstanceFor(XMPPConnection connection) { @@ -81,16 +92,25 @@ public final class PepManager extends Manager { return pepManager; } - private static final StanzaFilter FROM_BARE_JID_WITH_EVENT_EXTENSION_FILTER = new AndFilter( - new FromJidTypeFilter(JidType.BareJid), - EventExtensionFilter.INSTANCE); + // TODO: Ideally PepManager would re-use PubSubManager for this. But the functionality in PubSubManager does not yet + // exist. + private static final StanzaFilter PEP_EVENTS_FILTER = new AndFilter( + MessageTypeFilter.NORMAL_OR_HEADLINE, + FromJidTypeFilter.ENTITY_BARE_JID, + EventItemsExtensionFilter.INSTANCE); private final Set pepListeners = new CopyOnWriteArraySet<>(); private final AsyncButOrdered asyncButOrdered = new AsyncButOrdered<>(); + private final ServiceDiscoveryManager serviceDiscoveryManager; + private final PubSubManager pepPubSubManager; + private final MultiMap> pepEventListeners = new MultiMap<>(); + + private final Map, PepEventListenerCoupling> listenerToCouplingMap = new HashMap<>(); + /** * Creates a new PEP exchange manager. * @@ -98,6 +118,10 @@ public final class PepManager extends Manager { */ private PepManager(XMPPConnection connection) { super(connection); + + serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection); + pepPubSubManager = PubSubManager.getInstanceFor(connection, null); + StanzaListener packetListener = new StanzaListener() { @Override public void processStanza(Stanza stanza) { @@ -106,20 +130,118 @@ public final class PepManager extends Manager { assert event != null; final EntityBareJid from = message.getFrom().asEntityBareJidIfPossible(); assert from != null; + asyncButOrdered.performAsyncButOrdered(from, new Runnable() { @Override public void run() { + ItemsExtension itemsExtension = (ItemsExtension) event.getEvent(); + String node = itemsExtension.getNode(); + for (PepListener listener : pepListeners) { listener.eventReceived(from, event, message); } + + List> nodeListeners; + synchronized (pepEventListeners) { + nodeListeners = pepEventListeners.getAll(node); + if (nodeListeners.isEmpty()) { + return; + } + + // Make a copy of the list. Note that it is important to do this within the synchronized + // block. + nodeListeners = CollectionUtil.newListWith(nodeListeners); + } + + for (PepEventListenerCoupling listener : nodeListeners) { + // TODO: Can there be more than one item? + List items = itemsExtension.getItems(); + for (NamedElement namedElementItem : items) { + Item item = (Item) namedElementItem; + String id = item.getId(); + @SuppressWarnings("unchecked") + PayloadItem payloadItem = (PayloadItem) item; + ExtensionElement payload = payloadItem.getPayload(); + + listener.invoke(from, payload, id, message); + } + } } }); } }; // TODO Add filter to check if from supports PubSub as per xep163 2 2.4 - connection.addSyncStanzaListener(packetListener, FROM_BARE_JID_WITH_EVENT_EXTENSION_FILTER); + connection.addSyncStanzaListener(packetListener, PEP_EVENTS_FILTER); + } - pepPubSubManager = PubSubManager.getInstanceFor(connection, null); + private static final class PepEventListenerCoupling { + private final String node; + private final Class extensionElementType; + private final PepEventListener pepEventListener; + + private PepEventListenerCoupling(String node, Class extensionElementType, + PepEventListener pepEventListener) { + this.node = node; + this.extensionElementType = extensionElementType; + this.pepEventListener = pepEventListener; + } + + private void invoke(EntityBareJid from, ExtensionElement payload, String id, Message carrierMessage) { + if (!extensionElementType.isInstance(payload)) { + LOGGER.warning("Ignoring " + payload + " from " + carrierMessage + " as it is not of type " + + extensionElementType); + return; + } + + E extensionElementPayload = extensionElementType.cast(payload); + pepEventListener.onPepEvent(from, extensionElementPayload, id, carrierMessage); + } + } + + public boolean addPepEventListener(String node, Class extensionElementType, + PepEventListener pepEventListener) { + PepEventListenerCoupling pepEventListenerCoupling = new PepEventListenerCoupling<>(node, + extensionElementType, pepEventListener); + + synchronized (pepEventListeners) { + if (listenerToCouplingMap.containsKey(pepEventListener)) { + return false; + } + listenerToCouplingMap.put(pepEventListener, pepEventListenerCoupling); + /* + * TODO: Replace the above with the below using putIfAbsent() if Smack's minimum required Android SDK level + * is 24 or higher. PepEventListenerCoupling currentPepEventListenerCoupling = + * listenerToCouplingMap.putIfAbsent(pepEventListener, pepEventListenerCoupling); if + * (currentPepEventListenerCoupling != null) { return false; } + */ + + boolean listenerForNodeExisted = pepEventListeners.put(node, pepEventListenerCoupling); + if (!listenerForNodeExisted) { + serviceDiscoveryManager.addFeature(node + PubSubManager.PLUS_NOTIFY); + } + } + return true; + } + + public boolean removePepEventListener(PepEventListener pepEventListener) { + synchronized (pepEventListeners) { + PepEventListenerCoupling pepEventListenerCoupling = listenerToCouplingMap.remove(pepEventListener); + if (pepEventListenerCoupling == null) { + return false; + } + + String node = pepEventListenerCoupling.node; + + boolean mappingExisted = pepEventListeners.removeOne(node, pepEventListenerCoupling); + assert mappingExisted; + + if (!pepEventListeners.containsKey(pepEventListenerCoupling.node)) { + // This was the last listener for the node. Remove the +notify feature. + serviceDiscoveryManager.removeFeature(node + PubSubManager.PLUS_NOTIFY); + } + } + + return true; } public PubSubManager getPepPubSubManager() { @@ -127,8 +249,7 @@ public final class PepManager extends Manager { } /** - * Adds a listener to PEPs. The listener will be fired anytime PEP events - * are received from remote XMPP clients. + * Adds a listener to PEPs. The listener will be fired anytime PEP events are received from remote XMPP clients. * * @param pepListener a roster exchange listener. * @return true if pepListener was added. @@ -176,8 +297,8 @@ public final class PepManager extends Manager { // @formatter:on }; - public boolean isSupported() throws NoResponseException, XMPPErrorException, - NotConnectedException, InterruptedException { + public boolean isSupported() + throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { XMPPConnection connection = connection(); ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection); BareJid localBareJid = connection.getUser().asBareJid(); diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/ConfigurationEvent.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/ConfigurationEvent.java index db847372d..7f47847b5 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/ConfigurationEvent.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/ConfigurationEvent.java @@ -22,6 +22,8 @@ import java.util.List; import org.jivesoftware.smack.packet.ExtensionElement; +import org.jivesoftware.smackx.pubsub.form.FilledConfigureForm; + /** * Represents the configuration element of a PubSub message event which * 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 */ public class ConfigurationEvent extends NodeExtension implements EmbeddedPacketExtension { - private ConfigureForm form; + private final FilledConfigureForm form; 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); form = configForm; } - public ConfigureForm getConfiguration() { + public FilledConfigureForm getConfiguration() { return form; } @@ -50,6 +52,6 @@ public class ConfigurationEvent extends NodeExtension implements EmbeddedPacketE if (getConfiguration() == null) return Collections.emptyList(); else - return Arrays.asList((ExtensionElement) getConfiguration().getDataFormToSend()); + return Arrays.asList((ExtensionElement) getConfiguration().getDataForm()); } } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/ConfigureForm.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/ConfigureForm.java deleted file mode 100644 index 91921eee7..000000000 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/ConfigureForm.java +++ /dev/null @@ -1,681 +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}. - * - *

Unlike the {@link Form}.setAnswer(XXX)} methods, which throw an exception if the field does not - * exist, all ConfigureForm.setXXX 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 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 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 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 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 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())); - } - - /** - * List of the multi user chat rooms that are specified as reply rooms. - * - * @return The reply room JID's - */ - public List getReplyRoom() { - return getFieldValues(ConfigureNodeFields.replyroom); - } - - /** - * Sets the multi user chat rooms that are specified as reply rooms. - * - * @param replyRooms The multi user chat room to use as reply rooms - */ - public void setReplyRoom(List replyRooms) { - addField(ConfigureNodeFields.replyroom, FormField.Type.list_multi); - setAnswer(ConfigureNodeFields.replyroom.getFieldName(), replyRooms); - } - - /** - * Gets the specific JID's for reply to. - * - * @return The JID's - */ - public List getReplyTo() { - return getFieldValues(ConfigureNodeFields.replyto); - } - - /** - * Sets the specific JID's for reply to. - * - * @param replyTos The JID's to reply to - */ - public void setReplyTo(List replyTos) { - addField(ConfigureNodeFields.replyto, FormField.Type.list_multi); - setAnswer(ConfigureNodeFields.replyto.getFieldName(), replyTos); - } - - /** - * Gets the roster groups that are allowed to subscribe and retrieve items. - * - * @return The roster groups - */ - public List 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 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 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 getListSingle(String value) { - List list = new ArrayList<>(1); - list.add(value); - return list; - } - -} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/ConfigureNodeFields.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/ConfigureNodeFields.java index c828f061b..7ddf24468 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/ConfigureNodeFields.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/ConfigureNodeFields.java @@ -18,12 +18,13 @@ package org.jivesoftware.smackx.pubsub; 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 * 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 */ @@ -176,20 +177,6 @@ public enum ConfigureNodeFields { */ publish_model, - /** - * The specific multi-user chat rooms to specify for replyroom. - * - *

Value: List of JIDs as Strings

- */ - replyroom, - - /** - * The specific JID(s) to specify for replyto. - * - *

Value: List of JIDs as Strings

- */ - replyto, - /** * The roster group(s) allowed to subscribe and retrieve items. * diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/FormNode.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/FormNode.java index 9562dd4a0..6c3c69171 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/FormNode.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/FormNode.java @@ -16,7 +16,7 @@ */ 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 @@ -27,7 +27,7 @@ import org.jivesoftware.smackx.xdata.Form; * @author Robin Collier */ public class FormNode extends NodeExtension { - private final Form configForm; + private final DataForm configForm; /** * 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 submitForm The form */ - public FormNode(FormNodeType formType, Form submitForm) { + public FormNode(FormNodeType formType, DataForm submitForm) { super(formType.getNodeElement()); if (submitForm == null) @@ -51,7 +51,7 @@ public class FormNode extends NodeExtension { * @param nodeId The node the form is associated with * @param submitForm The form */ - public FormNode(FormNodeType formType, String nodeId, Form submitForm) { + public FormNode(FormNodeType formType, String nodeId, DataForm submitForm) { super(formType.getNodeElement(), nodeId); if (submitForm == null) @@ -64,7 +64,7 @@ public class FormNode extends NodeExtension { * * @return The form */ - public Form getForm() { + public DataForm getForm() { return configForm; } @@ -84,7 +84,7 @@ public class FormNode extends NodeExtension { } else builder.append('>'); - builder.append(configForm.getDataFormToSend().toXML()); + builder.append(configForm.toXML()); builder.append("'); return builder.toString(); diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/Item.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/Item.java index eaee0b168..727de6c48 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/Item.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/Item.java @@ -19,6 +19,7 @@ package org.jivesoftware.smackx.pubsub; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.util.XmlStringBuilder; +import org.jivesoftware.smackx.pubsub.form.ConfigureForm; import org.jivesoftware.smackx.pubsub.provider.ItemProvider; /** diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/ItemReply.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/ItemReply.java index 67051349d..eb93ec2d9 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/ItemReply.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/ItemReply.java @@ -16,8 +16,10 @@ */ 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. * * @author Robin Collier diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/ItemsExtension.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/ItemsExtension.java index 12d7a4aa6..000138897 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/ItemsExtension.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/ItemsExtension.java @@ -135,6 +135,7 @@ public class ItemsExtension extends NodeExtension implements EmbeddedPacketExten * * @return List of {@link Item}, {@link RetractItem}, or null */ + // TODO: Shouldn't this return List? Why is RetractItem not a subtype of item? public List getItems() { return items; } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/LeafNode.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/LeafNode.java index a6eb39650..e604a47d4 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/LeafNode.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/LeafNode.java @@ -27,6 +27,7 @@ import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.IQ.Type; import org.jivesoftware.smackx.disco.packet.DiscoverItems; +import org.jivesoftware.smackx.pubsub.form.ConfigureForm; import org.jivesoftware.smackx.pubsub.packet.PubSub; /** diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/Node.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/Node.java index 88decd520..313d0b93c 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/Node.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/Node.java @@ -37,6 +37,10 @@ import org.jivesoftware.smackx.delay.DelayInformationManager; import org.jivesoftware.smackx.disco.packet.DiscoverInfo; import org.jivesoftware.smackx.pubsub.Affiliation.AffiliationNamespace; 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.ItemEventListener; 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.shim.packet.Header; 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.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 - * via the {@link #sendConfigurationForm(Form)}. + * via the {@link #sendConfigurationForm(FillableConfigureForm)}. * * @return the configuration form * @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 NoResponseException if there was no response from the remote entity. * @throws NotConnectedException if the XMPP connection is not connected. * @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, - getId(), submitForm)); + getId(), configureForm.getDataFormToSubmit())); pubSubManager.getConnection().createStanzaCollectorAndSend(packet).nextResultOrThrow(); } @@ -454,9 +458,10 @@ public abstract class Node { * @throws NotConnectedException if the XMPP connection is not connected. * @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())); - request.addExtension(new FormNode(FormNodeType.OPTIONS, subForm)); + request.addExtension(new FormNode(FormNodeType.OPTIONS, submitForm)); PubSub reply = sendPubsubPacket(request); return reply.getExtension(PubSubElementType.SUBSCRIPTION); } @@ -483,11 +488,11 @@ public abstract class Node { * @throws NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. * @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 // 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; try { 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 - * via the {@link #sendConfigurationForm(Form)}. + * via the {@link #sendConfigurationForm(FillableConfigureForm)}. * * @param jid TODO javadoc me please * diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/NotificationType.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/NotificationType.java index 812f56b21..d915b2bc6 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/NotificationType.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/NotificationType.java @@ -16,9 +16,11 @@ */ package org.jivesoftware.smackx.pubsub; +import org.jivesoftware.smackx.pubsub.form.FillableConfigureForm; + /** * Specify the delivery style for event notifications. Denotes possible values - * for {@link ConfigureForm#setNotificationType(NotificationType)}. + * for {@link FillableConfigureForm#setNotificationType(NotificationType)}. * * @author Timothy Pitt */ diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/PayloadItem.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/PayloadItem.java index f2c9058aa..f22a8e876 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/PayloadItem.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/PayloadItem.java @@ -20,6 +20,7 @@ import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.util.XmlStringBuilder; +import org.jivesoftware.smackx.pubsub.form.ConfigureForm; import org.jivesoftware.smackx.pubsub.provider.ItemProvider; /** diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/PresenceState.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/PresenceState.java index e4fee0d65..7fbe9e094 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/PresenceState.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/PresenceState.java @@ -16,6 +16,8 @@ */ package org.jivesoftware.smackx.pubsub; +import org.jivesoftware.smackx.pubsub.form.SubscribeForm; + /** * Defines the possible valid presence states for node subscription via * {@link SubscribeForm#getShowValues()}. diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/PubSubManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/PubSubManager.java index 7d91e41ca..f328bde81 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/PubSubManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/PubSubManager.java @@ -46,11 +46,12 @@ import org.jivesoftware.smackx.disco.packet.DiscoverInfo; import org.jivesoftware.smackx.disco.packet.DiscoverItems; import org.jivesoftware.smackx.pubsub.PubSubException.NotALeafNodeException; 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.PubSubNamespace; import org.jivesoftware.smackx.pubsub.util.NodeUtils; -import org.jivesoftware.smackx.xdata.Form; -import org.jivesoftware.smackx.xdata.FormField; +import org.jivesoftware.smackx.xdata.packet.DataForm; import org.jxmpp.jid.BareJid; import org.jxmpp.jid.DomainBareJid; @@ -69,6 +70,8 @@ import org.jxmpp.stringprep.XmppStringprepException; */ public final class PubSubManager extends Manager { + public static final String PLUS_NOTIFY = "+notify"; + public static final String AUTO_CREATE_FEATURE = "http://jabber.org/protocol/pubsub#auto-create"; private static final Logger LOGGER = Logger.getLogger(PubSubManager.class.getName()); @@ -253,16 +256,18 @@ public final class PubSubManager extends Manager { * @throws NotConnectedException if the XMPP connection is not connected. * @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)); boolean isLeafNode = true; if (config != null) { - request.addExtension(new FormNode(FormNodeType.CONFIGURE, config)); - FormField nodeTypeField = config.getField(ConfigureNodeFields.node_type.getFieldName()); - - if (nodeTypeField != null) - isLeafNode = nodeTypeField.getValues().get(0).toString().equals(NodeType.leaf.toString()); + DataForm submitForm = config.getDataFormToSubmit(); + request.addExtension(new FormNode(FormNodeType.CONFIGURE, submitForm)); + NodeType nodeType = config.getNodeType(); + // Note that some implementations do to have the pubsub#node_type field in their defauilt configuration, + // 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 diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/PublishModel.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/PublishModel.java index f92ff24b3..321866cbc 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/PublishModel.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/PublishModel.java @@ -16,9 +16,11 @@ */ package org.jivesoftware.smackx.pubsub; +import org.jivesoftware.smackx.pubsub.form.FillableConfigureForm; + /** * Determines who may publish to a node. Denotes possible values - * for {@link ConfigureForm#setPublishModel(PublishModel)}. + * for {@link FillableConfigureForm#setPublishModel(PublishModel)}. * * @author Robin Collier */ diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/SubscribeForm.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/SubscribeForm.java deleted file mode 100644 index cdcaffaf5..000000000 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/SubscribeForm.java +++ /dev/null @@ -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}. - * - *

Unlike the {@link Form}.setAnswer(XXX)} methods, which throw an exception if the field does not - * exist, all SubscribeForm.setXXX 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 getShowValues() { - ArrayList 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 stateValues) { - ArrayList 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 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()); - } - } -} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/filter/EventItemsExtensionFilter.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/filter/EventItemsExtensionFilter.java new file mode 100644 index 000000000..03f87cd46 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/filter/EventItemsExtensionFilter.java @@ -0,0 +1,37 @@ +/** + * + * 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.filter; + +import org.jivesoftware.smack.filter.ExtensionElementFilter; + +import org.jivesoftware.smackx.pubsub.EventElement; +import org.jivesoftware.smackx.pubsub.EventElementType; + +public final class EventItemsExtensionFilter extends ExtensionElementFilter { + + public static final EventItemsExtensionFilter INSTANCE = new EventItemsExtensionFilter(); + + private EventItemsExtensionFilter() { + super(EventElement.class); + } + + @Override + public boolean accept(EventElement eventElement) { + EventElementType eventElementType = eventElement.getEventType(); + return eventElementType == EventElementType.items; + } +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/form/ConfigureForm.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/form/ConfigureForm.java new file mode 100644 index 000000000..f6a4c78b3 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/form/ConfigureForm.java @@ -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()); + } +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/form/ConfigureFormReader.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/form/ConfigureFormReader.java new file mode 100644 index 000000000..0184c41d8 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/form/ConfigureFormReader.java @@ -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 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 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 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 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()); + } +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/form/FillableConfigureForm.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/form/FillableConfigureForm.java new file mode 100644 index 000000000..0d7ebd039 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/form/FillableConfigureForm.java @@ -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 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 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 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 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); + } +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/form/FillableSubscribeForm.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/form/FillableSubscribeForm.java new file mode 100644 index 000000000..396678097 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/form/FillableSubscribeForm.java @@ -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 stateValues) { + ListMultiFormField.Builder builder = FormField.listMultiBuilder(SubscribeOptionFields.show_values.getFieldName()); + for (PresenceState state : stateValues) { + builder.addValue(state.toString()); + } + + write(builder.build()); + } +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/form/FilledConfigureForm.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/form/FilledConfigureForm.java new file mode 100644 index 000000000..74d2e48f1 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/form/FilledConfigureForm.java @@ -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); + } + +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/form/FilledSubscribeForm.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/form/FilledSubscribeForm.java new file mode 100644 index 000000000..67a82ad3a --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/form/FilledSubscribeForm.java @@ -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); + } + +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/form/SubscribeForm.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/form/SubscribeForm.java new file mode 100644 index 000000000..b1e173d5d --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/form/SubscribeForm.java @@ -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); + } + +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/form/SubscribeFormReader.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/form/SubscribeFormReader.java new file mode 100644 index 000000000..f6a691e58 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/form/SubscribeFormReader.java @@ -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 getShowValues() { + List values = readStringValues(SubscribeOptionFields.show_values.getFieldName()); + List result = new ArrayList<>(values.size()); + + for (String state : values) { + result.add(PresenceState.valueOf(state)); + } + return result; + } +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/form/package-info.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/form/package-info.java new file mode 100644 index 000000000..4a119d341 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/form/package-info.java @@ -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; diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/provider/ConfigEventProvider.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/provider/ConfigEventProvider.java index f13bd1621..1ae3324f8 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/provider/ConfigEventProvider.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/provider/ConfigEventProvider.java @@ -23,7 +23,7 @@ import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.provider.EmbeddedExtensionProvider; 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; /** @@ -38,6 +38,6 @@ public class ConfigEventProvider extends EmbeddedExtensionProvider { @Override protected FormNode createReturnExtension(String currentElement, String currentNamespace, Map attributeMap, List 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()); } } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/util/NodeUtils.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/util/NodeUtils.java index 9d386e609..d89cfee63 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/util/NodeUtils.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/util/NodeUtils.java @@ -18,10 +18,10 @@ package org.jivesoftware.smackx.pubsub.util; import org.jivesoftware.smack.packet.Stanza; -import org.jivesoftware.smackx.pubsub.ConfigureForm; import org.jivesoftware.smackx.pubsub.FormNode; 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. @@ -38,7 +38,7 @@ public class NodeUtils { */ public static ConfigureForm getFormFromPacket(Stanza packet, PubSubElementType elem) { FormNode config = (FormNode) packet.getExtensionElement(elem.getElementName(), elem.getNamespace().getXmlns()); - Form formReply = config.getForm(); - return new ConfigureForm(formReply); + DataForm dataForm = config.getForm(); + return new ConfigureForm(dataForm); } } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/search/ReportedData.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/search/ReportedData.java index 80c701e48..36853164b 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/search/ReportedData.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/search/ReportedData.java @@ -66,7 +66,7 @@ public class ReportedData { private ReportedData(DataForm dataForm) { // Add the columns to the report based on the reported data fields 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 @@ -76,7 +76,7 @@ public class ReportedData { // The field is created with all the values of the data form's field List values = new ArrayList<>(); values.addAll(field.getValues()); - fieldList.add(new Field(field.getVariable(), values)); + fieldList.add(new Field(field.getFieldName(), values)); } rows.add(new Row(fieldList)); } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/search/SimpleUserSearch.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/search/SimpleUserSearch.java index 702e494ea..ac7f36901 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/search/SimpleUserSearch.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/search/SimpleUserSearch.java @@ -24,8 +24,8 @@ import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.xml.XmlPullParser; 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; /** * 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 NAMESPACE = UserSearch.NAMESPACE; - private Form form; + private DataForm form; private ReportedData data; SimpleUserSearch() { super(ELEMENT, NAMESPACE); } - public void setForm(Form form) { + public void setForm(DataForm form) { this.form = form; } @@ -65,7 +65,7 @@ class SimpleUserSearch extends IQ { StringBuilder buf = new StringBuilder(); if (form == null) { - form = Form.getFormFrom(this); + form = DataForm.from(this); } if (form == null) { @@ -73,7 +73,7 @@ class SimpleUserSearch extends IQ { } for (FormField field : form.getFields()) { - String name = field.getVariable(); + String name = field.getFieldName(); String value = getSingleValue(field); if (value.trim().length() > 0) { buf.append('<').append(name).append('>').append(value).append("'); diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/search/UserSearch.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/search/UserSearch.java index 16167b293..bac591b78 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/search/UserSearch.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/search/UserSearch.java @@ -31,8 +31,6 @@ import org.jivesoftware.smack.util.PacketParserUtils; import org.jivesoftware.smack.xml.XmlPullParser; 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.jxmpp.jid.DomainBareJid; @@ -70,13 +68,13 @@ public class UserSearch extends SimpleIQ { * @throws NotConnectedException if the XMPP connection is not connected. * @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(); search.setType(IQ.Type.get); search.setTo(searchService); 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 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(); search.setType(IQ.Type.set); search.setTo(searchService); - search.addExtension(searchForm.getDataFormToSend()); + search.addExtension(searchForm); IQ response = con.createStanzaCollectorAndSend(search).nextResultOrThrow(); return ReportedData.getReportedDataFrom(response); @@ -113,7 +111,7 @@ public class UserSearch extends SimpleIQ { * @throws NotConnectedException if the XMPP connection is not connected. * @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(); search.setForm(searchForm); search.setType(IQ.Type.set); @@ -137,11 +135,7 @@ public class UserSearch extends SimpleIQ { boolean done = false; while (!done) { XmlPullParser.Event eventType = parser.next(); - if (eventType == XmlPullParser.Event.START_ELEMENT && parser.getName().equals("instructions")) { - buildDataForm(simpleUserSearch, parser.nextText(), parser, xmlEnvironment); - return simpleUserSearch; - } - else if (eventType == XmlPullParser.Event.START_ELEMENT && parser.getName().equals("item")) { + if (eventType == XmlPullParser.Event.START_ELEMENT && parser.getName().equals("item")) { simpleUserSearch.parseItems(parser); 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); - } - } - - } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/search/UserSearchManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/search/UserSearchManager.java index 85bcddd6f..866d66721 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/search/UserSearchManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/search/UserSearchManager.java @@ -24,7 +24,7 @@ import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPException.XMPPErrorException; import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; -import org.jivesoftware.smackx.xdata.Form; +import org.jivesoftware.smackx.xdata.packet.DataForm; import org.jxmpp.jid.DomainBareJid; @@ -71,7 +71,7 @@ public class UserSearchManager { * @throws NotConnectedException if the XMPP connection is not connected. * @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); } @@ -87,7 +87,7 @@ public class UserSearchManager { * @throws NotConnectedException if the XMPP connection is not connected. * @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); } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/usertune/UserTuneManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/usertune/UserTuneManager.java index 415c53dcc..7264b7227 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/usertune/UserTuneManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/usertune/UserTuneManager.java @@ -1,6 +1,6 @@ /** * - * Copyright 2019 Aditya Borikar. + * Copyright 2019 Aditya Borikar, 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. @@ -16,34 +16,23 @@ */ package org.jivesoftware.smackx.usertune; -import java.util.List; import java.util.Map; -import java.util.Set; import java.util.WeakHashMap; -import java.util.concurrent.CopyOnWriteArraySet; -import org.jivesoftware.smack.AsyncButOrdered; import org.jivesoftware.smack.Manager; import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.SmackException.NotLoggedInException; import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPException.XMPPErrorException; -import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.Message; -import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; -import org.jivesoftware.smackx.pep.PepListener; +import org.jivesoftware.smackx.pep.PepEventListener; import org.jivesoftware.smackx.pep.PepManager; -import org.jivesoftware.smackx.pubsub.EventElement; -import org.jivesoftware.smackx.pubsub.ItemsExtension; import org.jivesoftware.smackx.pubsub.PayloadItem; import org.jivesoftware.smackx.pubsub.PubSubException.NotALeafNodeException; import org.jivesoftware.smackx.usertune.element.UserTuneElement; -import org.jxmpp.jid.BareJid; -import org.jxmpp.jid.EntityBareJid; - /** * Entry point for Smacks API for XEP-0118: User Tune. *
@@ -51,7 +40,7 @@ import org.jxmpp.jid.EntityBareJid; *
* To stop publishing a UserTune, please use {@link #clearUserTune()} method. This will send a disabling publish signal. *
- * To add a UserTune listener in order to remain updated with other users UserTune, use {@link #addUserTuneListener(UserTuneListener)} method. + * To add a UserTune listener in order to remain updated with other users UserTune, use {@link #addUserTuneListener(PepEventListener)} method. *
* To link a UserTuneElement with {@link Message}, use 'message.addExtension(userTuneElement)'. *
@@ -63,15 +52,9 @@ import org.jxmpp.jid.EntityBareJid; public final class UserTuneManager extends Manager { public static final String USERTUNE_NODE = "http://jabber.org/protocol/tune"; - public static final String USERTUNE_NOTIFY = USERTUNE_NODE + "+notify"; private static final Map INSTANCES = new WeakHashMap<>(); - private static boolean ENABLE_USER_TUNE_NOTIFICATIONS_BY_DEFAULT = true; - - private final Set userTuneListeners = new CopyOnWriteArraySet<>(); - private final AsyncButOrdered asyncButOrdered = new AsyncButOrdered<>(); - private final ServiceDiscoveryManager serviceDiscoveryManager; private final PepManager pepManager; public static synchronized UserTuneManager getInstanceFor(XMPPConnection connection) throws NotLoggedInException { @@ -83,46 +66,9 @@ public final class UserTuneManager extends Manager { return manager; } - private UserTuneManager(XMPPConnection connection) throws NotLoggedInException { + private UserTuneManager(XMPPConnection connection) { super(connection); pepManager = PepManager.getInstanceFor(connection); - pepManager.addPepListener(new PepListener() { - @Override - public void eventReceived(EntityBareJid from, EventElement event, Message message) { - if (!USERTUNE_NODE.equals(event.getEvent().getNode())) { - return; - } - - final BareJid contact = from.asBareJid(); - asyncButOrdered.performAsyncButOrdered(contact, () -> { - ItemsExtension itemsExtension = (ItemsExtension) event.getEvent(); - List items = itemsExtension.getExtensions(); - @SuppressWarnings("unchecked") - PayloadItem payload = (PayloadItem) items.get(0); - UserTuneElement tune = payload.getPayload(); - - for (UserTuneListener listener : userTuneListeners) { - listener.onUserTuneUpdated(contact, message, tune); - } - }); - } - }); - serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection); - if (ENABLE_USER_TUNE_NOTIFICATIONS_BY_DEFAULT) { - enableUserTuneNotifications(); - } - } - - public static void setUserTuneNotificationsEnabledByDefault(boolean bool) { - ENABLE_USER_TUNE_NOTIFICATIONS_BY_DEFAULT = bool; - } - - public void enableUserTuneNotifications() { - serviceDiscoveryManager.addFeature(USERTUNE_NOTIFY); - } - - public void disableUserTuneNotifications() { - serviceDiscoveryManager.removeFeature(USERTUNE_NOTIFY); } public void clearUserTune() throws NotLoggedInException, NotALeafNodeException, NoResponseException, NotConnectedException, XMPPErrorException, InterruptedException { @@ -134,11 +80,11 @@ public final class UserTuneManager extends Manager { pepManager.publish(USERTUNE_NODE, new PayloadItem<>(userTuneElement)); } - public boolean addUserTuneListener(UserTuneListener listener) { - return userTuneListeners.add(listener); + public boolean addUserTuneListener(PepEventListener listener) { + return pepManager.addPepEventListener(USERTUNE_NODE, UserTuneElement.class, listener); } - public boolean removeUserTuneListener(UserTuneListener listener) { - return userTuneListeners.remove(listener); + public boolean removeUserTuneListener(PepEventListener listener) { + return pepManager.removePepEventListener(listener); } } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/AbstractMultiFormField.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/AbstractMultiFormField.java new file mode 100644 index 000000000..31c778a87 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/AbstractMultiFormField.java @@ -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 values; + + protected AbstractMultiFormField(Builder builder) { + super(builder); + values = CollectionUtil.cloneAndSeal(builder.values); + } + + @Override + public final List getValues() { + return values; + } + + + public abstract static class Builder> + extends FormField.Builder { + + private List 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 values) { + ensureValuesAreInitialized(); + + for (CharSequence value : values) { + this.values.add(value.toString()); + } + + return getThis(); + } + } +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/AbstractSingleStringValueFormField.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/AbstractSingleStringValueFormField.java new file mode 100644 index 000000000..27fca4cde --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/AbstractSingleStringValueFormField.java @@ -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> extends FormField.Builder { + + 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); + } + } +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/BooleanFormField.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/BooleanFormField.java new file mode 100644 index 000000000..2b036b162 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/BooleanFormField.java @@ -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 { + 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; + } + + } + +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/Form.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/Form.java deleted file mode 100644 index 9e5bb999a..000000000 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/Form.java +++ /dev/null @@ -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: - *

    - *
  • form → Indicates a form to fill out.
  • - *
  • submit → The form is filled out, and this is the data that is being returned from - * the form.
  • - *
  • cancel → The form was cancelled. Tell the asker that piece of information.
  • - *
  • result → Data results being returned from a search, or some other query.
  • - *
- * - * 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 XEP-0004 Data Forms - * - * @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 null 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.

- * - * 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.

- * - * 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.

- * - * 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.

- * - * 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 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 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 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 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.

- * - * 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 values = new ArrayList<>(); - values.addAll(field.getValues()); - form.setAnswer(field.getVariable(), values); - } - } - } - return form; - } - -} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/FormField.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/FormField.java index 525d8fa75..4bc28a7ef 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/FormField.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/FormField.java @@ -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"); * 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.HashCode; import org.jivesoftware.smack.util.MultiMap; +import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.util.XmlStringBuilder; 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, * 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. + *

+ * 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. + *

* * @author Gaston Dombiak */ -public final class FormField implements FullyQualifiedElement { +public abstract class FormField implements FullyQualifiedElement { 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 <field/>. */ - private final String variable; + private final String fieldName; 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 and hence also * appear in formFieldChildElements. */ - private final List