From d0a393118c1c6e4f1433f16e14e1ff42b6e49df8 Mon Sep 17 00:00:00 2001 From: akrherz Date: Sat, 11 Apr 2020 10:36:58 -0500 Subject: [PATCH 01/49] update Travis-CI badge on README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ----- From 7c2f9e3603370a421df9f0049d20fa59dc9b49c9 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 13 Apr 2020 12:14:32 +0200 Subject: [PATCH 02/49] pep: cleanup pep users API Use EntityBareJid just as its done within PepManager. There is no need for AsyncButOrdered in the PEP user managers, as PepManager already takes care of that. Also the message carrying the PEP event should always be the last parameter of the callbacks, as it is the least important piece of information. --- .../smackx/geoloc/GeoLocationListener.java | 4 +-- .../smackx/geoloc/GeoLocationManager.java | 24 ++++++-------- .../smackx/mood/MoodListener.java | 7 +++-- .../jivesoftware/smackx/mood/MoodManager.java | 31 +++++++------------ .../smackx/pubsub/ItemsExtension.java | 1 + .../smackx/usertune/UserTuneListener.java | 7 +++-- .../smackx/usertune/UserTuneManager.java | 22 +++++-------- .../GeolocationIntegrationTest.java | 4 +-- .../smackx/mood/MoodIntegrationTest.java | 2 +- .../usertune/UserTuneIntegrationTest.java | 4 +-- 10 files changed, 44 insertions(+), 62 deletions(-) diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/geoloc/GeoLocationListener.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/geoloc/GeoLocationListener.java index 8b1259ec7..25b6bb4d6 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/geoloc/GeoLocationListener.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/geoloc/GeoLocationListener.java @@ -19,8 +19,8 @@ package org.jivesoftware.smackx.geoloc; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smackx.geoloc.packet.GeoLocation; -import org.jxmpp.jid.BareJid; +import org.jxmpp.jid.EntityBareJid; public interface GeoLocationListener { - void onGeoLocationUpdated(BareJid jid, GeoLocation geoLocation, Message message); + void onGeoLocationUpdated(EntityBareJid jid, GeoLocation geoLocation, Message message); } 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..642e7f147 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. @@ -22,7 +22,6 @@ 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; @@ -43,7 +42,6 @@ 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; @@ -73,7 +71,6 @@ public final class GeoLocationManager extends Manager { 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; @@ -116,17 +113,14 @@ public final class GeoLocationManager extends Manager { 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); - } - }); + 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(from, geoLocation, message); + } } }); serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection); 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..243e67032 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, 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..54f50b4ee 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,11 @@ */ package org.jivesoftware.smackx.mood; -import java.util.HashSet; 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; import org.jivesoftware.smack.XMPPConnection; @@ -42,7 +41,6 @@ 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; /** @@ -72,8 +70,7 @@ public final class MoodManager extends Manager { private static final Map INSTANCES = new WeakHashMap<>(); - private final Set moodListeners = new HashSet<>(); - private final AsyncButOrdered asyncButOrdered = new AsyncButOrdered<>(); + private final Set moodListeners = new CopyOnWriteArraySet<>(); private PubSubManager pubSubManager; private MoodManager(XMPPConnection connection) { @@ -86,19 +83,13 @@ public final class MoodManager extends Manager { 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(); + 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); - } - } - }); + for (MoodListener listener : moodListeners) { + listener.onMoodUpdated(from, mood, message); + } } }); } @@ -170,11 +161,11 @@ public final class MoodManager extends Manager { message.addExtension(element); } - public synchronized void addMoodListener(MoodListener listener) { + public void addMoodListener(MoodListener listener) { moodListeners.add(listener); } - public synchronized void removeMoodListener(MoodListener listener) { + public void removeMoodListener(MoodListener listener) { moodListeners.remove(listener); } } 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/usertune/UserTuneListener.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/usertune/UserTuneListener.java index e020e4ec2..3e053776b 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/usertune/UserTuneListener.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/usertune/UserTuneListener.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. @@ -20,9 +20,10 @@ import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smackx.usertune.element.UserTuneElement; -import org.jxmpp.jid.BareJid; +import org.jxmpp.jid.EntityBareJid; public interface UserTuneListener { - void onUserTuneUpdated(BareJid jid, Message message, UserTuneElement userTuneElement); + void onUserTuneUpdated(EntityBareJid jid, UserTuneElement userTuneElement, Message message); + } 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..9b1a131af 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 @@ -22,7 +22,6 @@ 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; @@ -41,7 +40,6 @@ 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; /** @@ -70,7 +68,6 @@ public final class UserTuneManager extends Manager { 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; @@ -93,18 +90,15 @@ public final class UserTuneManager extends Manager { 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(); + 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); - } - }); + for (UserTuneListener listener : userTuneListeners) { + listener.onUserTuneUpdated(from, tune, message); + } } }); serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/geolocation/GeolocationIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/geolocation/GeolocationIntegrationTest.java index 3909cee88..f6db34dae 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/geolocation/GeolocationIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/geolocation/GeolocationIntegrationTest.java @@ -35,7 +35,7 @@ import org.igniterealtime.smack.inttest.annotations.AfterClass; import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; import org.igniterealtime.smack.inttest.util.IntegrationTestRosterUtil; import org.igniterealtime.smack.inttest.util.SimpleResultSyncPoint; -import org.jxmpp.jid.BareJid; +import org.jxmpp.jid.EntityBareJid; import org.jxmpp.util.XmppDateTime; public class GeolocationIntegrationTest extends AbstractSmackIntegrationTest { @@ -82,7 +82,7 @@ public class GeolocationIntegrationTest extends AbstractSmackIntegrationTest { final GeoLocationListener geoLocationListener = new GeoLocationListener() { @Override - public void onGeoLocationUpdated(BareJid jid, GeoLocation geoLocation, Message message) { + public void onGeoLocationUpdated(EntityBareJid jid, GeoLocation geoLocation, Message message) { if (geoLocation.equals(geoLocation1)) { geoLocationReceived.signal(); } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/mood/MoodIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/mood/MoodIntegrationTest.java index 3338bc4f2..faccc9a83 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/mood/MoodIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/mood/MoodIntegrationTest.java @@ -43,7 +43,7 @@ public class MoodIntegrationTest extends AbstractSmackIntegrationTest { final SimpleResultSyncPoint moodReceived = new SimpleResultSyncPoint(); - final MoodListener moodListener = (jid, message, moodElement) -> { + final MoodListener moodListener = (jid, moodElement, message) -> { if (moodElement.getMood() == Mood.satisfied) { moodReceived.signal(); } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/usertune/UserTuneIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/usertune/UserTuneIntegrationTest.java index cae6c1d8d..45ddec6f0 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/usertune/UserTuneIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/usertune/UserTuneIntegrationTest.java @@ -31,7 +31,7 @@ import org.igniterealtime.smack.inttest.annotations.AfterClass; import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; import org.igniterealtime.smack.inttest.util.IntegrationTestRosterUtil; import org.igniterealtime.smack.inttest.util.SimpleResultSyncPoint; -import org.jxmpp.jid.BareJid; +import org.jxmpp.jid.EntityBareJid; public class UserTuneIntegrationTest extends AbstractSmackIntegrationTest { @@ -63,7 +63,7 @@ public class UserTuneIntegrationTest extends AbstractSmackIntegrationTest { final UserTuneListener userTuneListener = new UserTuneListener() { @Override - public void onUserTuneUpdated(BareJid jid, Message message, UserTuneElement userTuneElement) { + public void onUserTuneUpdated(EntityBareJid jid, UserTuneElement userTuneElement, Message message) { if (userTuneElement.equals(userTuneElement1)) { userTuneReceived.signal(); } From 340bcb2d12dbd0c7431a4dd112ceaa87ff7d82e3 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 13 Apr 2020 15:26:46 +0200 Subject: [PATCH 03/49] pep: improve API, add PepEventListener The geoloc, mood and usertune PEP users showed a pattern. Instead of repeating this pattern every time, let PepManager do the hard work --- .../smackx/geoloc/GeoLocationManager.java | 73 ++-------- .../smackx/mood/MoodListener.java | 2 +- .../jivesoftware/smackx/mood/MoodManager.java | 51 ++----- .../PepEventListener.java} | 12 +- .../jivesoftware/smackx/pep/PepManager.java | 131 +++++++++++++++++- .../smackx/pubsub/PubSubManager.java | 2 + .../smackx/usertune/UserTuneListener.java | 29 ---- .../smackx/usertune/UserTuneManager.java | 64 ++------- .../GeolocationIntegrationTest.java | 8 +- .../smackx/mood/MoodIntegrationTest.java | 5 +- .../usertune/UserTuneIntegrationTest.java | 5 +- 11 files changed, 178 insertions(+), 204 deletions(-) rename smack-extensions/src/main/java/org/jivesoftware/smackx/{geoloc/GeoLocationListener.java => pep/PepEventListener.java} (70%) delete mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/usertune/UserTuneListener.java 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 642e7f147..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 @@ -16,11 +16,8 @@ */ 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.ConnectionCreationListener; import org.jivesoftware.smack.Manager; @@ -29,30 +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.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)`. *
@@ -63,15 +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 ServiceDiscoveryManager serviceDiscoveryManager; private final PepManager pepManager; static { @@ -105,28 +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; - } - - 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(from, geoLocation, message); - } - } - }); - serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection); - if (ENABLE_USER_LOCATION_NOTIFICATIONS_BY_DEFAULT) { - enableUserLocationNotifications(); - } } public void sendGeoLocationToJid(GeoLocation geoLocation, Jid jid) throws InterruptedException, @@ -154,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)); } /** @@ -179,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/mood/MoodListener.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/mood/MoodListener.java index 243e67032..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 @@ -24,6 +24,6 @@ import org.jxmpp.jid.EntityBareJid; public interface MoodListener { - void onMoodUpdated(EntityBareJid from, MoodElement moodElement, Message message); + 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 54f50b4ee..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 @@ -17,9 +17,7 @@ package org.jivesoftware.smackx.mood; import java.util.Map; -import java.util.Set; import java.util.WeakHashMap; -import java.util.concurrent.CopyOnWriteArraySet; import org.jivesoftware.smack.Manager; import org.jivesoftware.smack.SmackException; @@ -28,20 +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.EntityBareJid; /** * Entry point for Smacks API for XEP-0107: User Mood. @@ -49,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()}.
*
@@ -66,32 +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 CopyOnWriteArraySet<>(); - 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; - } - 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(from, mood, message); - } - } - }); + pepManager = PepManager.getInstanceFor(connection); } public static synchronized MoodManager getInstanceFor(XMPPConnection connection) { @@ -138,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) { @@ -161,11 +130,11 @@ public final class MoodManager extends Manager { message.addExtension(element); } - public void addMoodListener(MoodListener listener) { - moodListeners.add(listener); + public boolean addMoodListener(PepEventListener listener) { + return pepManager.addPepEventListener(MOOD_NODE, MoodElement.class, listener); } - public 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/geoloc/GeoLocationListener.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pep/PepEventListener.java similarity index 70% rename from smack-extensions/src/main/java/org/jivesoftware/smackx/geoloc/GeoLocationListener.java rename to smack-extensions/src/main/java/org/jivesoftware/smackx/pep/PepEventListener.java index 25b6bb4d6..6abf2e77f 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/geoloc/GeoLocationListener.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pep/PepEventListener.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,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.jivesoftware.smackx.geoloc; +package org.jivesoftware.smackx.pep; +import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.Message; -import org.jivesoftware.smackx.geoloc.packet.GeoLocation; import org.jxmpp.jid.EntityBareJid; -public interface GeoLocationListener { - void onGeoLocationUpdated(EntityBareJid jid, GeoLocation geoLocation, Message message); +public interface PepEventListener { + + void onPepEvent(EntityBareJid from, E event, String id, Message carrierMessage); + } 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..7a480503e 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; @@ -33,13 +36,19 @@ import org.jivesoftware.smack.filter.AndFilter; 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; @@ -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,6 +92,7 @@ public final class PepManager extends Manager { return pepManager; } + // TODO: Filter only for event extensions with as child. private static final StanzaFilter FROM_BARE_JID_WITH_EVENT_EXTENSION_FILTER = new AndFilter( new FromJidTypeFilter(JidType.BareJid), EventExtensionFilter.INSTANCE); @@ -89,8 +101,14 @@ public final class PepManager extends Manager { 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 +116,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 +128,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); + } - 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 +247,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 +295,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/PubSubManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/PubSubManager.java index 7d91e41ca..1b9b28a55 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 @@ -69,6 +69,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()); diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/usertune/UserTuneListener.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/usertune/UserTuneListener.java deleted file mode 100644 index 3e053776b..000000000 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/usertune/UserTuneListener.java +++ /dev/null @@ -1,29 +0,0 @@ -/** - * - * 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. - * 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.usertune; - -import org.jivesoftware.smack.packet.Message; - -import org.jivesoftware.smackx.usertune.element.UserTuneElement; - -import org.jxmpp.jid.EntityBareJid; - -public interface UserTuneListener { - - void onUserTuneUpdated(EntityBareJid jid, UserTuneElement userTuneElement, Message message); - -} 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 9b1a131af..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,11 +16,8 @@ */ 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.Manager; import org.jivesoftware.smack.SmackException.NoResponseException; @@ -28,20 +25,14 @@ 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.EntityBareJid; - /** * Entry point for Smacks API for XEP-0118: User Tune. *
@@ -49,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)'. *
@@ -61,14 +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 ServiceDiscoveryManager serviceDiscoveryManager; private final PepManager pepManager; public static synchronized UserTuneManager getInstanceFor(XMPPConnection connection) throws NotLoggedInException { @@ -80,43 +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; - } - - 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(from, tune, message); - } - } - }); - 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 { @@ -128,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-integration-test/src/main/java/org/jivesoftware/smackx/geolocation/GeolocationIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/geolocation/GeolocationIntegrationTest.java index f6db34dae..af86bb996 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/geolocation/GeolocationIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/geolocation/GeolocationIntegrationTest.java @@ -25,9 +25,9 @@ import org.jivesoftware.smack.SmackException.NotLoggedInException; import org.jivesoftware.smack.XMPPException.XMPPErrorException; import org.jivesoftware.smack.packet.Message; -import org.jivesoftware.smackx.geoloc.GeoLocationListener; import org.jivesoftware.smackx.geoloc.GeoLocationManager; import org.jivesoftware.smackx.geoloc.packet.GeoLocation; +import org.jivesoftware.smackx.pep.PepEventListener; import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; @@ -79,10 +79,10 @@ public class GeolocationIntegrationTest extends AbstractSmackIntegrationTest { IntegrationTestRosterUtil.ensureBothAccountsAreSubscribedToEachOther(conOne, conTwo, timeout); final SimpleResultSyncPoint geoLocationReceived = new SimpleResultSyncPoint(); - final GeoLocationListener geoLocationListener = new GeoLocationListener() { + final PepEventListener geoLocationListener = new PepEventListener() { @Override - public void onGeoLocationUpdated(EntityBareJid jid, GeoLocation geoLocation, Message message) { + public void onPepEvent(EntityBareJid jid, GeoLocation geoLocation, String id, Message message) { if (geoLocation.equals(geoLocation1)) { geoLocationReceived.signal(); } @@ -92,7 +92,7 @@ public class GeolocationIntegrationTest extends AbstractSmackIntegrationTest { glm2.addGeoLocationListener(geoLocationListener); try { - glm1.sendGeolocation(geoLocation1); + glm1.publishGeoLocation(geoLocation1); geoLocationReceived.waitForResult(timeout); } finally { glm2.removeGeoLocationListener(geoLocationListener); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/mood/MoodIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/mood/MoodIntegrationTest.java index faccc9a83..d2eff1080 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/mood/MoodIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/mood/MoodIntegrationTest.java @@ -19,6 +19,9 @@ package org.jivesoftware.smackx.mood; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smackx.mood.element.MoodElement; +import org.jivesoftware.smackx.pep.PepEventListener; + import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; import org.igniterealtime.smack.inttest.annotations.AfterClass; @@ -43,7 +46,7 @@ public class MoodIntegrationTest extends AbstractSmackIntegrationTest { final SimpleResultSyncPoint moodReceived = new SimpleResultSyncPoint(); - final MoodListener moodListener = (jid, moodElement, message) -> { + final PepEventListener moodListener = (jid, moodElement, id, message) -> { if (moodElement.getMood() == Mood.satisfied) { moodReceived.signal(); } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/usertune/UserTuneIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/usertune/UserTuneIntegrationTest.java index 45ddec6f0..0309f9af0 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/usertune/UserTuneIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/usertune/UserTuneIntegrationTest.java @@ -23,6 +23,7 @@ import org.jivesoftware.smack.SmackException.NotLoggedInException; import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smackx.pep.PepEventListener; import org.jivesoftware.smackx.usertune.element.UserTuneElement; import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest; @@ -61,9 +62,9 @@ public class UserTuneIntegrationTest extends AbstractSmackIntegrationTest { final SimpleResultSyncPoint userTuneReceived = new SimpleResultSyncPoint(); - final UserTuneListener userTuneListener = new UserTuneListener() { + final PepEventListener userTuneListener = new PepEventListener() { @Override - public void onUserTuneUpdated(EntityBareJid jid, UserTuneElement userTuneElement, Message message) { + public void onPepEvent(EntityBareJid jid, UserTuneElement userTuneElement, String id, Message message) { if (userTuneElement.equals(userTuneElement1)) { userTuneReceived.signal(); } From 6d9936a0a6ffb5e6308c94dcd4a9d2b3c141f8f5 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 13 Apr 2020 15:27:43 +0200 Subject: [PATCH 04/49] geoloc: do not set error in integration test As error is deprecated. And should be marked as such. --- .../smackx/geolocation/GeolocationIntegrationTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/geolocation/GeolocationIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/geolocation/GeolocationIntegrationTest.java index af86bb996..2707a5435 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/geolocation/GeolocationIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/geolocation/GeolocationIntegrationTest.java @@ -61,7 +61,6 @@ public class GeolocationIntegrationTest extends AbstractSmackIntegrationTest { .setCountry("India") .setCountryCode("IN") .setDescription("My Description") - .setError(90d) .setFloor("top") .setLat(25.098345d) .setLocality("awesome") From 9b20e2efd8565ed932c63157bf0db1c96e5c5bd6 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 13 Apr 2020 15:28:17 +0200 Subject: [PATCH 05/49] sinttest: signal failure if geoloc element does not match --- .../smackx/geolocation/GeolocationIntegrationTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/geolocation/GeolocationIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/geolocation/GeolocationIntegrationTest.java index 2707a5435..32f1d32b3 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/geolocation/GeolocationIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/geolocation/GeolocationIntegrationTest.java @@ -84,6 +84,8 @@ public class GeolocationIntegrationTest extends AbstractSmackIntegrationTest { public void onPepEvent(EntityBareJid jid, GeoLocation geoLocation, String id, Message message) { if (geoLocation.equals(geoLocation1)) { geoLocationReceived.signal(); + } else { + geoLocationReceived.signalFailure("Received non matching GeoLocation"); } } }; From 72a9cb65a63c499a02686bdb64e5761df4b7c15b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 16 Dec 2019 07:13:16 +0100 Subject: [PATCH 06/49] OriginIdElement: Add proper equals() method --- .../smackx/sid/element/OriginIdElement.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) 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(); + } } From e0f7ddf5a873587082fa2293956fdc757377234f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 13 Apr 2020 18:17:26 +0200 Subject: [PATCH 07/49] Add support for XEP-0422: Message Fastening SMACK-884 --- documentation/extensions/index.md | 1 + .../MessageFasteningManager.java | 107 ++++++ .../element/ExternalElement.java | 84 +++++ .../element/FasteningElement.java | 325 ++++++++++++++++++ .../element/package-info.java | 25 ++ .../message_fastening/package-info.java | 25 ++ .../provider/FasteningElementProvider.java | 80 +++++ .../provider/package-info.java | 25 ++ .../experimental.providers | 6 + .../experimental.xml | 1 + .../MessageFasteningElementsTest.java | 229 ++++++++++++ 11 files changed, 908 insertions(+) create mode 100644 smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/MessageFasteningManager.java create mode 100644 smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/element/ExternalElement.java create mode 100644 smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/element/FasteningElement.java create mode 100644 smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/element/package-info.java create mode 100644 smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/package-info.java create mode 100644 smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/provider/FasteningElementProvider.java create mode 100644 smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/provider/package-info.java create mode 100644 smack-experimental/src/test/java/org/jivesoftware/smackx/message_fastening/MessageFasteningElementsTest.java diff --git a/documentation/extensions/index.md b/documentation/extensions/index.md index 47100f37f..f84b5c025 100644 --- a/documentation/extensions/index.md +++ b/documentation/extensions/index.md @@ -120,6 +120,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-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..ee73afae1 --- /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 = true; + + 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/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/message_fastening/MessageFasteningElementsTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/message_fastening/MessageFasteningElementsTest.java new file mode 100644 index 000000000..9764b0363 --- /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)); + } +} From 905d5dc102bd721c566e4db2c59d5b271d74d593 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 13 Apr 2020 18:18:31 +0200 Subject: [PATCH 08/49] Fix typo in XmppElementUtil --- .../main/java/org/jivesoftware/smack/util/XmppElementUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); From f5c412a98f47ded4ba5610a67a5381cff35dcd2e Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 13 Apr 2020 19:59:22 +0200 Subject: [PATCH 09/49] geoloc: GeoLocation constructor should have Builder as sole paramter Also remove that (broken) "Error and accuracy set" warning, but mark (get|set)Error() as deprecated. --- .../smackx/geoloc/packet/GeoLocation.java | 96 ++++++++----------- .../geoloc/provider/GeoLocationProvider.java | 10 +- .../smackx/geoloc/packet/GeoLocationTest.java | 1 + .../provider/GeoLocationProviderTest.java | 18 ++-- 4 files changed, 62 insertions(+), 63 deletions(-) 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..61285a11f 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,11 @@ 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.XmlStringBuilder; import org.jivesoftware.smackx.xdata.FormField; @@ -50,8 +47,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 +72,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 +139,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; } @@ -318,7 +301,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 +440,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 +599,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/test/java/org/jivesoftware/smackx/geoloc/packet/GeoLocationTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/geoloc/packet/GeoLocationTest.java index f8af5da0b..60003cb36 100644 --- a/smack-extensions/src/test/java/org/jivesoftware/smackx/geoloc/packet/GeoLocationTest.java +++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/geoloc/packet/GeoLocationTest.java @@ -121,6 +121,7 @@ public class GeoLocationTest extends SmackTestSuite { assertNotNull(geoLocation); assertNotNull(geoLocation.toXML()); + @SuppressWarnings("deprecation") GeoLocation constructedGeoLocation = GeoLocation.builder().setAccuracy(23d).setAlt(1000d).setAltAccuracy(10d).setArea("Delhi").setBearing( 10d).setBuilding("Small Building").setCountry("India").setCountryCode("IN").setDescription( "My Description").setError(90d).setFloor("top").setLat(25.098345d).setLocality("awesome").setLon( diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/geoloc/provider/GeoLocationProviderTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/geoloc/provider/GeoLocationProviderTest.java index 01738fa3e..ee18cd493 100644 --- a/smack-extensions/src/test/java/org/jivesoftware/smackx/geoloc/provider/GeoLocationProviderTest.java +++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/geoloc/provider/GeoLocationProviderTest.java @@ -18,7 +18,6 @@ package org.jivesoftware.smackx.geoloc.provider; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; import java.net.URI; @@ -82,7 +81,9 @@ public class GeoLocationProviderTest extends SmackTestSuite { assertEquals("IN", geoLocation.getCountryCode()); assertEquals("WGS84", geoLocation.getDatum()); assertEquals("My Description", geoLocation.getDescription()); - assertNull(geoLocation.getError()); + @SuppressWarnings("deprecation") + Double error = geoLocation.getError(); + assertEquals(90, error); assertEquals("top", geoLocation.getFloor()); assertEquals((Double) 25.098345d, geoLocation.getLat()); assertEquals("awesome", geoLocation.getLocality()); @@ -150,7 +151,9 @@ public class GeoLocationProviderTest extends SmackTestSuite { assertEquals("IN", geoLocation.getCountryCode()); assertEquals("Test Datum", geoLocation.getDatum()); assertEquals("My Description", geoLocation.getDescription()); - assertNull(geoLocation.getError()); + @SuppressWarnings("deprecation") + Double error = geoLocation.getError(); + assertEquals(90, error); assertEquals("top", geoLocation.getFloor()); assertEquals((Double) 25.098345d, geoLocation.getLat()); assertEquals("awesome", geoLocation.getLocality()); @@ -183,7 +186,9 @@ public class GeoLocationProviderTest extends SmackTestSuite { GeoLocation geoLocation = messageWithGeoLocation.getExtension(GeoLocation.class); - assertEquals((Double) 90d, geoLocation.getError()); + @SuppressWarnings("deprecation") + Double error = geoLocation.getError(); + assertEquals((Double) 90d, error); } @Test @@ -223,8 +228,9 @@ public class GeoLocationProviderTest extends SmackTestSuite { GeoLocation geoLocation = messageWithGeoLocation.getExtension(GeoLocation.class); assertEquals((Double) 90d, geoLocation.getAccuracy()); - assertNull(geoLocation.getError()); - + @SuppressWarnings("deprecation") + Double error = geoLocation.getError(); + assertEquals(100, error); } } From 4f609b855c067c08359306d857fdc24d3b7382d6 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 13 Apr 2020 20:00:36 +0200 Subject: [PATCH 10/49] geoloc: make GeoLocation implement hashCode() and equals(Object) --- .../smackx/geoloc/packet/GeoLocation.java | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) 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 61285a11f..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 @@ -24,6 +24,8 @@ import javax.xml.namespace.QName; import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.util.EqualsUtil; +import org.jivesoftware.smack.util.HashCode; import org.jivesoftware.smack.util.XmlStringBuilder; import org.jivesoftware.smackx.xdata.FormField; @@ -249,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 From c49999b9334b68433db0cd60ef391fd70cb00c0b Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 13 Apr 2020 20:01:20 +0200 Subject: [PATCH 11/49] core: add shortcut via hash in EqualsUtil Return false as soon as the hashed value does not match. This is sound, since every class that implements equals(Object) should also implement hashCode(). --- .../main/java/org/jivesoftware/smack/util/EqualsUtil.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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)); From aea95d3401447954f5bb2d7a73f6135626fa2f4e Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 13 Apr 2020 20:46:59 +0200 Subject: [PATCH 12/49] sinttest: also check for length of subdescriptions varargs --- .../smack/inttest/SmackIntegrationTestFramework.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFramework.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFramework.java index 006aa61d8..0f6767a45 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFramework.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFramework.java @@ -746,7 +746,7 @@ public class SmackIntegrationTestFramework { .append(method.getName()) .append(" (") .append(testType.name()); - if (subdescriptons != null) { + if (subdescriptons != null && subdescriptons.length > 0) { sb.append(", "); StringUtils.appendTo(Arrays.asList(subdescriptons), sb); } From 988954a9db1a2d8f3cf60ac3cd1b5d15d08cfe40 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 13 Apr 2020 20:52:07 +0200 Subject: [PATCH 13/49] core: delete deprecated filters Those where deprecated in 2015 with d4a6d8e65 ("Rename PacketFilter (and implementing classes) and PacketExtension"), now it is time to delete them. --- .../smack/filter/PacketExtensionFilter.java | 79 ------------------- .../smack/filter/PacketFilter.java | 51 ------------ .../smack/filter/PacketIDFilter.java | 66 ---------------- .../smack/filter/PacketTypeFilter.java | 63 --------------- 4 files changed, 259 deletions(-) delete mode 100644 smack-core/src/main/java/org/jivesoftware/smack/filter/PacketExtensionFilter.java delete mode 100644 smack-core/src/main/java/org/jivesoftware/smack/filter/PacketFilter.java delete mode 100644 smack-core/src/main/java/org/jivesoftware/smack/filter/PacketIDFilter.java delete mode 100644 smack-core/src/main/java/org/jivesoftware/smack/filter/PacketTypeFilter.java 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(); - } -} From fb7054bbe7d3e60567fde22812997427b855d3a0 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 13 Apr 2020 20:54:04 +0200 Subject: [PATCH 14/49] core: delete deprecated ToFilter This filter was marked deprecated in 2017 with 5d0dd49e6 ("Introduce ToMatchesFilter"), time to delete it. --- .../jivesoftware/smack/filter/ToFilter.java | 50 ------------------- 1 file changed, 50 deletions(-) delete mode 100644 smack-core/src/main/java/org/jivesoftware/smack/filter/ToFilter.java 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; - } -} From 5114f6dfa4a46408829e7c889feef685ae1e8e5c Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 13 Apr 2020 21:25:28 +0200 Subject: [PATCH 15/49] core: remove deprecated methods in PacketUtil Those where deprecated in 2015 with 8409dddff ("Add PacketUtil.extensionElementFrom()"), and in 2017 with 2288825b1 ("Retain smack-core API"). --- .../jivesoftware/smack/util/PacketUtil.java | 36 +------------------ 1 file changed, 1 insertion(+), 35 deletions(-) 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. * From 50da46ffda56be89277ff2ad287e17251849878c Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 13 Apr 2020 21:27:31 +0200 Subject: [PATCH 16/49] core: Add ExtensionElementFilter --- .../smack/filter/ExtensionElementFilter.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/filter/ExtensionElementFilter.java 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; + } +} From 2c6f444bab6940e03c98b98bc2a05d1034a8daaf Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 13 Apr 2020 21:27:45 +0200 Subject: [PATCH 17/49] pubsub: Add EventItemsExtensionFilter --- .../filter/EventItemsExtensionFilter.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/filter/EventItemsExtensionFilter.java 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; + } +} From 7f027bd339e5b0d5dadda98152066ea8078f1982 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 13 Apr 2020 21:27:56 +0200 Subject: [PATCH 18/49] pep: Use EventItemsExtensionFilter --- .../main/java/org/jivesoftware/smackx/pep/PepManager.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 7a480503e..c830fcbf7 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 @@ -52,7 +52,7 @@ 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; @@ -92,10 +92,9 @@ public final class PepManager extends Manager { return pepManager; } - // TODO: Filter only for event extensions with as child. private static final StanzaFilter FROM_BARE_JID_WITH_EVENT_EXTENSION_FILTER = new AndFilter( new FromJidTypeFilter(JidType.BareJid), - EventExtensionFilter.INSTANCE); + EventItemsExtensionFilter.INSTANCE); private final Set pepListeners = new CopyOnWriteArraySet<>(); From 6c3cd535672f7259e651beced24517b9659a95f0 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 13 Apr 2020 22:36:36 +0200 Subject: [PATCH 19/49] pep: improve pep event filter --- .../smack/filter/jidtype/FromJidTypeFilter.java | 2 ++ .../java/org/jivesoftware/smackx/pep/PepManager.java | 11 +++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) 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-extensions/src/main/java/org/jivesoftware/smackx/pep/PepManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pep/PepManager.java index c830fcbf7..0ec19bf8a 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 @@ -33,8 +33,8 @@ 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; @@ -92,8 +92,11 @@ 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), + // 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.HEADLINE, + FromJidTypeFilter.ENTITY_BARE_JID, EventItemsExtensionFilter.INSTANCE); private final Set pepListeners = new CopyOnWriteArraySet<>(); @@ -168,7 +171,7 @@ public final class PepManager extends Manager { } }; // 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); } private static final class PepEventListenerCoupling { From dd248adb287be273061cc23411ade348a3afc62e Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 13 Apr 2020 22:49:31 +0200 Subject: [PATCH 20/49] sinttest: delcare boolean in WaitForClosingStreamElementTest --- .../jivesoftware/smack/WaitForClosingStreamElementTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smack/WaitForClosingStreamElementTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smack/WaitForClosingStreamElementTest.java index 65bf5a5a4..023979d64 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smack/WaitForClosingStreamElementTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smack/WaitForClosingStreamElementTest.java @@ -43,6 +43,7 @@ public class WaitForClosingStreamElementTest extends AbstractSmackLowLevelIntegr if (failureException != null) { throw new AssertionError("Sync poing yielded failure exception", failureException); } - assertTrue(closingStreamReceived.wasSuccessful()); + boolean closingStreamReceivedSuccessful = closingStreamReceived.wasSuccessful(); + assertTrue(closingStreamReceivedSuccessful); } } From f3e93cef32470df728fd047262959a58a8ad4590 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 13 Apr 2020 22:50:02 +0200 Subject: [PATCH 21/49] core: do not init() closingStreamReceived sync point in initState() The initState() method is also called in disconnect(). And if we reset the closingStreamReceived sync point at disconnect, it will break the WaitForClosingStreamElementTest integration test. --- .../java/org/jivesoftware/smack/AbstractXMPPConnection.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 { From cbc2024875ae8c4af5b0ee23ccb01e33ae56ec3b Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Tue, 14 Apr 2020 09:20:43 +0200 Subject: [PATCH 22/49] sinttest: print smack version --- .../smack/inttest/SmackIntegrationTestFramework.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFramework.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFramework.java index 0f6767a45..b297445a4 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFramework.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFramework.java @@ -171,7 +171,7 @@ public class SmackIntegrationTestFramework { // Create a connection manager *after* we created the testRunId (in testRunResult). this.connectionManager = new XmppConnectionManager(this); - LOGGER.info("SmackIntegrationTestFramework [" + testRunResult.testRunId + ']' + ": Starting"); + LOGGER.info("SmackIntegrationTestFramework [" + testRunResult.testRunId + ']' + ": Starting\nSmack version: " + SmackConfiguration.getVersion()); if (config.debugger != Configuration.Debugger.none) { // JUL Debugger will not print any information until configured to print log messages of // level FINE From 38c77fd5731742de438c7c071f50db872d7859bc Mon Sep 17 00:00:00 2001 From: adiaholic Date: Tue, 14 Apr 2020 16:16:04 +0530 Subject: [PATCH 23/49] Correct erroneous documentation --- documentation/developer/integrationtest.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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` From 77d12d475816ff81508a60a4201beeabd7749251 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Wed, 15 Apr 2020 09:34:04 +0200 Subject: [PATCH 24/49] fastening: set ENABLED_BY_DEFAULT to false If it is announced as feature, entities sending fastened messages expect the recipient to react somehow on those. And this is not the case if this is just enabled in Smack. Hence we disable it per default and require smack users to explicitly enable it after they have setup the according stanza listeners. --- .../smackx/message_fastening/MessageFasteningManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index ee73afae1..d66cd1bff 100644 --- 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 @@ -51,7 +51,7 @@ public final class MessageFasteningManager extends Manager { public static final String NAMESPACE = "urn:xmpp:fasten:0"; - private static boolean ENABLED_BY_DEFAULT = true; + private static boolean ENABLED_BY_DEFAULT = false; private static final WeakHashMap INSTANCES = new WeakHashMap<>(); From da5f59a99642b13c494e6019733aa2e301f0b499 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Wed, 15 Apr 2020 09:35:13 +0200 Subject: [PATCH 25/49] Remove superfluous ' "" +' statements Using sed -i 's; "" +;;' to remove those. --- .../message_fastening/MessageFasteningElementsTest.java | 8 ++++---- .../test/java/org/jivesoftware/smackx/ox/TestKeys.java | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) 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 index 9764b0363..8228fccfb 100644 --- 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 @@ -57,7 +57,7 @@ public class MessageFasteningElementsTest { */ @Test public void fasteningElementSerializationTest() { - String xml = "" + + String xml = "" + " " + ""; @@ -73,7 +73,7 @@ public class MessageFasteningElementsTest { @ParameterizedTest @EnumSource(SmackTestUtil.XmlPullParserKind.class) public void fasteningDeserializationTest(SmackTestUtil.XmlPullParserKind parserKind) throws XmlPullParserException, IOException, SmackParsingException { - String xml = "" + + String xml = "" + " " + " " + @@ -102,7 +102,7 @@ public class MessageFasteningElementsTest { @Test public void fasteningDeserializationClearTest() throws XmlPullParserException, IOException, SmackParsingException { - String xml = "" + + String xml = "" + " " + ""; @@ -114,7 +114,7 @@ public class MessageFasteningElementsTest { @Test public void fasteningElementWithExternalElementsTest() { - String xml = "" + + String xml = "" + " " + " " + diff --git a/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox/TestKeys.java b/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox/TestKeys.java index f7b756bce..d6c28bf23 100644 --- a/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox/TestKeys.java +++ b/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox/TestKeys.java @@ -43,7 +43,7 @@ public class TestKeys { /** * Public key of xmpp:juliet@capulet.lit. */ - public static final String JULIET_PUB = "" + + public static final String JULIET_PUB = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "\n" + "mQENBFrxov4BCAChZwPrBxxIlwzpieR5T2pnaOZLWH0WqSON6rVjvfbJHWdDi3Th\n" + @@ -66,7 +66,7 @@ public class TestKeys { /** * Private key of xmpp:juliet@capulet.lit. */ - public static final String JULIET_PRIV = "" + + public static final String JULIET_PRIV = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + "\n" + "lQOYBFrxov4BCAChZwPrBxxIlwzpieR5T2pnaOZLWH0WqSON6rVjvfbJHWdDi3Th\n" + @@ -104,7 +104,7 @@ public class TestKeys { /** * Public key of xmpp:romeo@montague.lit. */ - public static final String ROMEO_PUB = "" + + public static final String ROMEO_PUB = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "\n" + "mQENBFrxopkBCADiYg/+mEObXgxuMW6/LFKpEyaJK9pBMgutuxnYZ9PXWZmOhDIT\n" + @@ -127,7 +127,7 @@ public class TestKeys { /** * Private key of xmpp:romeo@montague.lit. */ - public static final String ROMEO_PRIV = "" + + public static final String ROMEO_PRIV = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + "\n" + "lQOYBFrxopkBCADiYg/+mEObXgxuMW6/LFKpEyaJK9pBMgutuxnYZ9PXWZmOhDIT\n" + From 162651821e4a5a6253d54d5b8559d220099a0a03 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Wed, 15 Apr 2020 19:44:16 +0200 Subject: [PATCH 26/49] sinttest: log unexpected Throwables thrown by runTests() Because we would not see those if the finally block also threw. --- .../smack/inttest/SmackIntegrationTestFramework.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFramework.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFramework.java index b297445a4..b89402df8 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFramework.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFramework.java @@ -227,6 +227,11 @@ public class SmackIntegrationTestFramework { try { runTests(classes); } + catch (Throwable t) { + // Log the thrown Throwable to prevent it being shadowed in case the finally block below also throws. + LOGGER.log(Level.SEVERE, "Unexpected abort because runTests() threw throwable", t); + throw t; + } finally { // Ensure that the accounts are deleted and disconnected before we continue connectionManager.disconnectAndCleanup(); From fa643f12d5deef6a1e782d660159e4b298196e3f Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Thu, 16 Apr 2020 21:20:35 +0200 Subject: [PATCH 27/49] muc: add MUC status code 33 SMACK-882 --- .../main/java/org/jivesoftware/smackx/muc/packet/MUCUser.java | 1 + 1 file changed, 1 insertion(+) 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..eda58e90e 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; From e2e228fc93ef0cbdd31cde119413d1561a251bd0 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Thu, 16 Apr 2020 21:20:53 +0200 Subject: [PATCH 28/49] muc: synchronize Stats.create(Integer) Since this method is used by the MUCUserProvider, it is potentially invoked concurrently and the access to the statusMap must be synchronized then. --- .../org/jivesoftware/smackx/muc/packet/MUCUser.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) 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 eda58e90e..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 @@ -420,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; } From c519dd1213bd96d9ff5a92d8f64e3957c7b8d981 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Thu, 16 Apr 2020 21:38:55 +0200 Subject: [PATCH 29/49] muc: do check for equality twice We already performed the presence.getFrom().equals(myRoomJID) check and saved its result. No need to do it again. --- .../main/java/org/jivesoftware/smackx/muc/MultiUserChat.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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..3d87c9644 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 @@ -236,7 +236,7 @@ public class MultiUserChat { // Fire events according to the received presence code checkPresenceCode( mucUser.getStatus(), - presence.getFrom().equals(myRoomJID), + isUserStatusModification, mucUser, from); } else { From ab2822be3ef31e2598d2607c3ecc95b3dc849679 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Thu, 16 Apr 2020 21:39:52 +0200 Subject: [PATCH 30/49] muc: also set myRoomJid to null if we have left the room --- .../src/main/java/org/jivesoftware/smackx/muc/MultiUserChat.java | 1 + 1 file changed, 1 insertion(+) 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 3d87c9644..8dc709558 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 @@ -2120,6 +2120,7 @@ public class MultiUserChat { // presence. occupantsMap.clear(); joined = false; + myRoomJid = null; // Update the list of joined rooms multiUserChatManager.removeJoinedRoom(room); removeConnectionCallbacks(); From 26ab8324520f10aa8009f5054ee862c77ba1925c Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Thu, 16 Apr 2020 21:40:06 +0200 Subject: [PATCH 31/49] muc: only call userHasLeft() at one site There is no need to duplicate that code. Also ensure that userHasLeft() is invoked *before* the listeners are invoked, so that e.g. isJoined() returns false in the listeners. --- .../smackx/muc/MultiUserChat.java | 23 +++---------------- 1 file changed, 3 insertions(+), 20 deletions(-) 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 8dc709558..68624f7f1 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 @@ -233,6 +233,9 @@ 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(), @@ -2438,9 +2441,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()); } @@ -2455,15 +2455,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) { @@ -2475,15 +2469,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 @@ -2498,11 +2486,6 @@ public class MultiUserChat { for (UserStatusListener listener : userStatusListeners) { listener.roomDestroyed(alternateMUC, mucUser.getDestroy().getReason()); } - - // Reset occupant information. - occupantsMap.clear(); - myRoomJid = null; - userHasLeft(); } } From 20aaef26289fcaa95fd8ecd23ad907f5a371a021 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Thu, 16 Apr 2020 21:44:55 +0200 Subject: [PATCH 32/49] muc: remove 'joined' boolean from MultiUserChat, use myRoomJid instead If myRoomJid is set, we are joined. --- .../smackx/muc/MultiUserChat.java | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) 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 68624f7f1..9cc409c38 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 @@ -149,7 +149,6 @@ public class MultiUserChat { private String subject; private EntityFullJid myRoomJid; - private boolean joined = false; private StanzaCollector messageCollector; MultiUserChat(XMPPConnection connection, EntityBareJid room, MultiUserChatManager multiUserChatManager) { @@ -386,8 +385,6 @@ public class MultiUserChat { Resourcepart receivedNickname = presence.getFrom().getResourceOrThrow(); setNickname(receivedNickname); - joined = true; - // Update the list of joined rooms multiUserChatManager.addJoinedRoom(room); return presence; @@ -439,7 +436,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(); } @@ -494,7 +491,7 @@ public class MultiUserChat { */ public synchronized MucCreateConfigFormHandle createOrJoin(MucEnterConfiguration mucEnterConfiguration) throws NoResponseException, XMPPErrorException, InterruptedException, MucAlreadyJoinedException, NotConnectedException, NotAMucServiceException { - if (joined) { + if (isJoined()) { throw new MucAlreadyJoinedException(); } @@ -665,7 +662,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(); } @@ -683,7 +680,7 @@ public class MultiUserChat { * @return true if currently in the multi user chat room. */ public boolean isJoined() { - return joined; + return myRoomJid != null; } /** @@ -1138,7 +1135,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); @@ -1181,11 +1178,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() @@ -2122,7 +2114,6 @@ 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); From 0f7b7df1f0cd6a3d2f75a983143d02fda8990111 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Thu, 16 Apr 2020 21:59:19 +0200 Subject: [PATCH 33/49] muc: fix roomDestroyed() callback The previous site where the callback was invoked was only reached if there was also a user status on the unvailable presence. But those are not part of unavilable presences upon room destruction. Fixes SMACK-888. --- .../smackx/muc/MultiUserChat.java | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) 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 9cc409c38..7c2d0950b 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 @@ -248,6 +248,22 @@ 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()); + } + } } break; default: @@ -2471,13 +2487,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()); - } - } } /** From c3c14cfdb9c08f535ddbb38f7599515a666dbe34 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Fri, 17 Apr 2020 10:29:40 +0200 Subject: [PATCH 34/49] gradle: remove nonStrictJavadocProjects This become obsolete with 1a3067c89 ("Enable werror for javadoc and fix javadoc issues"), which enabled 'werror' for javadoc on all projects. --- build.gradle | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/build.gradle b/build.gradle index b0a81ba9f..5ced307b4 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 @@ -571,15 +557,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" From 22cff274bb895b338f256b7f6c081169c77e877c Mon Sep 17 00:00:00 2001 From: adiaholic Date: Fri, 17 Apr 2020 20:10:45 +0530 Subject: [PATCH 35/49] Remove unexpected `MucNotJoinedException` from `MultiUserChat.leave()` Occupant information should be reset after `leavePresence` and `reflectedLeavePresenceFilter` are built. --- .../java/org/jivesoftware/smackx/muc/MultiUserChat.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 7c2d0950b..8c8b50647 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 @@ -732,10 +732,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); @@ -757,6 +753,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; From c07f41c1194a66879ed26cae56721117e85458da Mon Sep 17 00:00:00 2001 From: Jesus Date: Sat, 11 Apr 2020 12:17:42 -0500 Subject: [PATCH 36/49] debug: show active tab on Smack debugger startup Enhanced Debugger Window showed Smack Info tab, now shows active connection. Fixes SMACK-367. --- .../org/jivesoftware/smackx/debugger/EnhancedDebuggerWindow.java | 1 + 1 file changed, 1 insertion(+) 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 From d0347d1e0063a5dd3a51e8784b51acd7e3440118 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Fri, 17 Apr 2020 22:26:28 +0200 Subject: [PATCH 37/49] muc: add removed() callback to UserStatusListener --- .../smackx/muc/MultiUserChat.java | 5 ++++ .../smackx/muc/UserStatusListener.java | 29 +++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) 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 8c8b50647..0a2ce3060 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 @@ -265,6 +265,11 @@ public class MultiUserChat { } } } + if (isUserStatusModification) { + for (UserStatusListener listener : userStatusListeners) { + listener.removed(mucUser, presence); + } + } break; default: break; 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..a727ee3cb 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.5 + */ + 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); From 8e1003723e164e7c7278153f71fd9ebc27dc0854 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Sat, 18 Apr 2020 13:09:41 +0200 Subject: [PATCH 38/49] Specify correct and full version in @since javadoc tag The next release of Smack will be 4.4.0. --- .../java/org/jivesoftware/smackx/muc/MucEnterConfiguration.java | 2 +- .../java/org/jivesoftware/smackx/muc/UserStatusListener.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/UserStatusListener.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/UserStatusListener.java index a727ee3cb..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 @@ -78,7 +78,7 @@ public interface UserStatusListener { * * @param mucUser the optional muc#user extension element * @param presence the carrier presence - * @since 4.5 + * @since 4.4.0 */ default void removed(MUCUser mucUser, Presence presence) { }; From e58e6fa75dfb5cf4f95059da91651caf0031d30f Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Sat, 18 Apr 2020 19:01:25 +0200 Subject: [PATCH 39/49] xdata: add more helper methods to DataForm --- .../smackx/xdata/packet/DataForm.java | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/packet/DataForm.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/packet/DataForm.java index cbe796492..3415041e4 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/packet/DataForm.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/packet/DataForm.java @@ -20,6 +20,7 @@ package org.jivesoftware.smackx.xdata.packet; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; @@ -303,6 +304,20 @@ public class DataForm implements ExtensionElement { return Collections.unmodifiableList(extensionElements); } + /** + * Return the form type from the hidden form type field. + * + * @return the form type or null if this form has none set. + * @since 4.4.0 + */ + public String getFormType() { + FormField formTypeField = getHiddenFormTypeField(); + if (formTypeField == null) { + return null; + } + return formTypeField.getFirstValue(); + } + /** * Returns the hidden FORM_TYPE field or null if this data form has none. * @@ -364,6 +379,56 @@ public class DataForm implements ExtensionElement { return stanzaView.getExtension(DataForm.class); } + /** + * Get the data form with the given form type from a stanza view. + * + * @param stanzaView the stanza view to retrieve the data form from + * @param formType the form type + * @return the retrieved data form or null if there is no matching one + * @since 4.4.0 + */ + public static DataForm from(StanzaView stanzaView, String formType) { + List dataForms = stanzaView.getExtensions(DataForm.class); + return from(dataForms, formType); + } + + /** + * Return the first matching data form with the given form type from the given collection of data forms. + * + * @param dataForms the collection of data forms + * @param formType the form type to match for + * @return the first matching data form or null if there is none + * @since 4.4.0 + */ + public static DataForm from(Collection dataForms, String formType) { + for (DataForm dataForm : dataForms) { + if (formType.equals(dataForm.getFormType())) { + return dataForm; + } + } + return null; + } + + /** + * Remove the first matching data form with the given form type from the given collection. + * + * @param dataForms the collection of data forms + * @param formType the form type to match for + * @return the removed data form or null if there was none removed + * @since 4.4.0 + */ + public static DataForm remove(Collection dataForms, String formType) { + Iterator it = dataForms.iterator(); + while (it.hasNext()) { + DataForm dataForm = it.next(); + if (formType.equals(dataForm.getFormType())) { + it.remove(); + return dataForm; + } + } + return null; + } + /** * * Represents the fields that will be returned from a search. This information is useful when From 661b2743d90b50ba24987bb4782c87802a5d08ab Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Sat, 18 Apr 2020 19:03:00 +0200 Subject: [PATCH 40/49] mam: use new DataForm API in MamQueryIQ Use the new API introduced with e58e6fa75 ("xdata: add more helper methods to DataForm") in MamQueryIQ. --- .../org/jivesoftware/smackx/mam/element/MamQueryIQ.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) 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 + "'"); } From cdc5396f6c7e3307ab6b4c84fa54586f2bc7d9cc Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Sat, 18 Apr 2020 19:03:43 +0200 Subject: [PATCH 41/49] core: improve signature of Stanza.addExtensions() --- .../src/main/java/org/jivesoftware/smack/packet/Stanza.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); From dc443bccd495c9e6b881ccb2c7a657482bd220c7 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Sat, 18 Apr 2020 19:04:21 +0200 Subject: [PATCH 42/49] disco: allow multiple data forms for extended discovery information Previously Smack only supported a single data form as extended discovery information. --- .../smackx/caps/EntityCapsManager.java | 4 +- .../AbstractNodeInformationProvider.java | 4 +- .../smackx/disco/NodeInformationProvider.java | 2 +- .../smackx/disco/ServiceDiscoveryManager.java | 100 +++++++++++++----- 4 files changed, 77 insertions(+), 33 deletions(-) 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..84fbb59e3 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 @@ -548,7 +548,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 +558,7 @@ public final class EntityCapsManager extends Manager { return identities; } @Override - public List getNodePacketExtensions() { + public List getNodePacketExtensions() { return packetExtensions; } }); 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(); + } } /** From 5e921e639358c419658e30ad4cc4af189d205892 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Sat, 18 Apr 2020 22:56:10 +0200 Subject: [PATCH 43/49] core: add javadoc for StanzaView.getExtensions(Class) --- .../java/org/jivesoftware/smack/packet/StanzaView.java | 7 +++++++ 1 file changed, 7 insertions(+) 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); } From e79710840be6d6b3301e078a23688eafaa06013c Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Sat, 18 Apr 2020 22:56:24 +0200 Subject: [PATCH 44/49] caps: handle multiple data forms of extened service discovery information --- .../smackx/caps/EntityCapsManager.java | 76 +++++++++---------- 1 file changed, 36 insertions(+), 40 deletions(-) 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 84fbb59e3..008baa3bd 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 @@ -19,6 +19,7 @@ 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.LinkedList; @@ -664,8 +665,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 +704,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.getVariable().compareTo(f2.getVariable()); } + }); - // 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.getVariable().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.getVariable()); + 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 From f61ecb65e7a7d31c5ea1485ac7067e91f2237114 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Fri, 24 Apr 2020 10:04:30 +0200 Subject: [PATCH 45/49] Make java(c|doc) use --release when available In order to truely stay Java 8 compatible, declaring a source and target compatiblity is not sufficient. Source compatiblity means that the input, i.e. the code written in Java is compatible with that particular version of the Java Language Specification (JLS). And target compatibitliy means that the produced Java bytecode is compatible with that particular version of the Java Virtual Machine Specificiation (JVMS). But there is actually a third dimension: the runtime library (rt.jar). If signatures of methods change over java releases within the runtime library, then the produced bytecode, may contain calls to methods that do not exist with that exact same signature in older java versions. For example the family of Buffer subclasses changed the return value of certain functions, for example flip() to not return Buffer, but the concrete type of the current instance, e.g. CharBuffer. If we compile now with a newer JDK, where the return type is CharBuffer and not Buffer, then executing on an older JDK, where the return type is Buffer, then we get java.lang.NoSuchMethodError(s) thrown at us. Fixes SMACK-651. --- .travis.yml | 3 +-- build.gradle | 26 +++++++++++++++++--------- 2 files changed, 18 insertions(+), 11 deletions(-) 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/build.gradle b/build.gradle index 5ced307b4..76cf80d61 100644 --- a/build.gradle +++ b/build.gradle @@ -122,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) { @@ -244,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' } @@ -290,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[] From 2c39dff653e77b35d94f9fbc6216bbdcdfeee5ac Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 4 May 2020 10:55:36 +0200 Subject: [PATCH 46/49] pubsub: remove 'replyto' and 'replyroom' configure settings Those configurations where removed with version 1.13 (2010-07-12) of XEP-0060. This change is part of the effort to upgrade Smack's PubSub implementation (SMACK-364). --- .../smackx/pubsub/ConfigureForm.java | 38 ------------------- .../smackx/pubsub/ConfigureNodeFields.java | 14 ------- 2 files changed, 52 deletions(-) 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 index 91921eee7..01787ea03 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/ConfigureForm.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/ConfigureForm.java @@ -494,44 +494,6 @@ public class ConfigureForm extends Form { 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. * 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..30ef27c25 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 @@ -176,20 +176,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. * From 3270c113c50ebce4e7ddae85fe44d85000a3877f Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 11 May 2020 22:12:22 +0200 Subject: [PATCH 47/49] [filetransfer] Remove FaultTolerantNegotiator The FaultTolerantNegotiator is the reason why Smack replies in a non-standard way to file transfer requests: Smack puts two values in the stream-method field, while the field is a list-single field, i.e. a field which only allows one value. Even if what Smack does is probably better, as it allows for a fallback in case the bytestream transport fails, it is not standard compliant. And, Jingle provide a proper fallback specification for file transfers. Fixes SMACK-561. --- .../filetransfer/FaultTolerantNegotiator.java | 111 ------------------ .../filetransfer/FileTransferNegotiator.java | 13 +- .../filetransfer/IBBTransferNegotiator.java | 4 +- .../Socks5TransferNegotiator.java | 4 +- .../smackx/filetransfer/StreamNegotiator.java | 12 +- 5 files changed, 11 insertions(+), 133 deletions(-) delete mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/filetransfer/FaultTolerantNegotiator.java 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..e1575df8d 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 @@ -239,12 +239,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 +350,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 { 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..2aaf4c73b 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 @@ -69,11 +69,11 @@ 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()); @@ -83,9 +83,7 @@ public abstract class StreamNegotiator extends Manager { DataForm form = new DataForm(DataForm.Type.submit); FormField.Builder field = FormField.builder( FileTransferNegotiator.STREAM_DATA_FIELD_NAME); - for (String namespace : namespaces) { - field.addValue(namespace); - } + field.addValue(namespace); form.addField(field.build()); response.setFeatureNegotiationForm(form); @@ -95,7 +93,7 @@ public abstract class StreamNegotiator extends Manager { 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 +180,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); From 577c59484b14726e553eb31d745b9fe27616cedd Mon Sep 17 00:00:00 2001 From: Guus der Kinderen Date: Tue, 12 May 2020 19:57:56 +0200 Subject: [PATCH 48/49] pep: notification type can be normal or headline --- .../java/org/jivesoftware/smack/filter/MessageTypeFilter.java | 1 + .../src/main/java/org/jivesoftware/smackx/pep/PepManager.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) 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-extensions/src/main/java/org/jivesoftware/smackx/pep/PepManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pep/PepManager.java index 0ec19bf8a..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 @@ -95,7 +95,7 @@ public final class PepManager extends Manager { // 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.HEADLINE, + MessageTypeFilter.NORMAL_OR_HEADLINE, FromJidTypeFilter.ENTITY_BARE_JID, EventItemsExtensionFilter.INSTANCE); From 77e26fc5754a102d37372b5a977d718a36294dca Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Wed, 13 May 2020 20:14:41 +0200 Subject: [PATCH 49/49] Re-work data form API Apply builder pattern to form fields and replace getVariable() with getFieldName(). Refer to the field name as "field name" instead of "variable" everyone, just as XEP-0004 does. Improve the high-level form API: introduce FilledForm and FillableForm which perform stronger validation and consistency checks. Also add FormFieldRegistry to enable processing of 'submit' forms where the form field types are omitted. Smack also now does omit the form field type declaration on 'submit' type forms, as it is allowed by XEP-0004. --- documentation/extensions/index.md | 1 + .../smack/parsing/SmackParsingException.java | 6 +- .../smack/util/CollectionUtil.java | 17 + .../jivesoftware/smack/util/StringUtils.java | 9 + .../smack/util/XmlStringBuilder.java | 11 + .../jivesoftware/smackx/mam/MamManager.java | 25 +- .../element/EnablePushNotificationsIQ.java | 14 +- .../jivesoftware/smackx/mam/FiltersTest.java | 4 +- .../smackx/mam/MamQueryIQProviderTest.java | 2 +- .../org/jivesoftware/smackx/mam/MamTest.java | 3 +- .../jivesoftware/smackx/mam/PagingTest.java | 2 +- .../smackx/mam/QueryArchiveTest.java | 2 +- .../smackx/mam/ResultsLimitTest.java | 4 +- .../smackx/mam/RetrieveFormFieldsTest.java | 15 +- .../admin/ServiceAdministrationManager.java | 8 +- .../smackx/caps/EntityCapsManager.java | 51 +- .../smackx/commands/AdHocCommand.java | 20 +- .../smackx/commands/AdHocCommandManager.java | 9 +- .../smackx/commands/RemoteCommand.java | 22 +- .../filetransfer/FileTransferNegotiator.java | 18 +- .../smackx/filetransfer/StreamNegotiator.java | 9 +- .../smackx/formtypes/FormFieldRegistry.java | 158 +++++ .../smackx/formtypes/package-info.java | 21 + .../smackx/muc/MucConfigFormManager.java | 39 +- .../smackx/muc/MultiUserChat.java | 48 +- .../smackx/muc/MultiUserChatConstants.java | 23 + .../org/jivesoftware/smackx/muc/RoomInfo.java | 10 +- .../smackx/offline/OfflineMessageManager.java | 14 +- .../smackx/pubsub/ConfigurationEvent.java | 12 +- .../smackx/pubsub/ConfigureForm.java | 643 ------------------ .../smackx/pubsub/ConfigureNodeFields.java | 5 +- .../jivesoftware/smackx/pubsub/FormNode.java | 12 +- .../org/jivesoftware/smackx/pubsub/Item.java | 1 + .../jivesoftware/smackx/pubsub/ItemReply.java | 4 +- .../jivesoftware/smackx/pubsub/LeafNode.java | 1 + .../org/jivesoftware/smackx/pubsub/Node.java | 27 +- .../smackx/pubsub/NotificationType.java | 4 +- .../smackx/pubsub/PayloadItem.java | 1 + .../smackx/pubsub/PresenceState.java | 2 + .../smackx/pubsub/PubSubManager.java | 19 +- .../smackx/pubsub/PublishModel.java | 4 +- .../smackx/pubsub/SubscribeForm.java | 214 ------ .../smackx/pubsub/form/ConfigureForm.java | 33 + .../pubsub/form/ConfigureFormReader.java | 292 ++++++++ .../pubsub/form/FillableConfigureForm.java | 314 +++++++++ .../pubsub/form/FillableSubscribeForm.java | 95 +++ .../pubsub/form/FilledConfigureForm.java | 29 + .../pubsub/form/FilledSubscribeForm.java | 29 + .../smackx/pubsub/form/SubscribeForm.java | 29 + .../pubsub/form/SubscribeFormReader.java | 95 +++ .../smackx/pubsub/form/package-info.java | 21 + .../pubsub/provider/ConfigEventProvider.java | 4 +- .../pubsub/provider/FormNodeProvider.java | 3 +- .../smackx/pubsub/util/NodeUtils.java | 8 +- .../smackx/search/ReportedData.java | 4 +- .../smackx/search/SimpleUserSearch.java | 10 +- .../smackx/search/UserSearch.java | 64 +- .../smackx/search/UserSearchManager.java | 6 +- .../smackx/xdata/AbstractMultiFormField.java | 92 +++ .../AbstractSingleStringValueFormField.java | 100 +++ .../smackx/xdata/BooleanFormField.java | 98 +++ .../org/jivesoftware/smackx/xdata/Form.java | 517 -------------- .../jivesoftware/smackx/xdata/FormField.java | 416 +++++------ .../smackx/xdata/FormFieldChildElement.java | 7 +- .../smackx/xdata/FormFieldWithOptions.java | 47 ++ .../smackx/xdata/JidMultiFormField.java | 94 +++ .../smackx/xdata/JidSingleFormField.java | 71 ++ .../smackx/xdata/ListMultiFormField.java | 88 +++ .../smackx/xdata/ListSingleFormField.java | 87 +++ .../smackx/xdata/SingleValueFormField.java | 48 ++ .../smackx/xdata/TextMultiFormField.java | 73 ++ .../smackx/xdata/TextSingleFormField.java | 51 ++ .../smackx/xdata/XDataManager.java | 10 +- .../smackx/xdata/form/FillableForm.java | 267 ++++++++ .../smackx/xdata/form/FilledForm.java | 97 +++ .../jivesoftware/smackx/xdata/form/Form.java | 44 ++ .../smackx/xdata/form/FormReader.java | 90 +++ .../smackx/xdata/form/FormWriter.java | 25 + .../smackx/xdata/form/package-info.java | 21 + .../smackx/xdata/packet/DataForm.java | 486 ++++++++----- .../xdata/provider/DataFormProvider.java | 257 ++++++- .../smackx/xdata/provider/OptionProvider.java | 63 -- .../xdata/provider/RequiredProvider.java | 43 -- .../smackx/xdata/provider/ValueProvider.java | 43 -- .../packet/ValidateElement.java | 51 +- .../smackx/caps/EntityCapsManagerTest.java | 122 ++-- .../jivesoftware/smackx/muc/RoomInfoTest.java | 19 +- .../smackx/pubsub/ConfigureFormTest.java | 16 - .../smackx/xdata/FormFieldTest.java | 38 ++ .../smackx/xdata/packet/DataFormTest.java | 66 +- .../DataValidationHelperTest.java | 15 +- .../smackx/pubsub/PubSubIntegrationTest.java | 7 +- .../jivesoftware/smackx/xdata/FormTest.java | 64 +- .../smackx/workgroup/agent/AgentSession.java | 5 +- .../agent/TranscriptSearchManager.java | 11 +- .../smackx/workgroup/user/Workgroup.java | 31 +- .../smackx/ox/util/OpenPgpPubSubUtil.java | 6 +- 97 files changed, 3809 insertions(+), 2427 deletions(-) create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/formtypes/FormFieldRegistry.java create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/formtypes/package-info.java create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatConstants.java delete mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/ConfigureForm.java delete mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/SubscribeForm.java create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/form/ConfigureForm.java create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/form/ConfigureFormReader.java create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/form/FillableConfigureForm.java create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/form/FillableSubscribeForm.java create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/form/FilledConfigureForm.java create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/form/FilledSubscribeForm.java create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/form/SubscribeForm.java create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/form/SubscribeFormReader.java create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/form/package-info.java create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/AbstractMultiFormField.java create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/AbstractSingleStringValueFormField.java create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/BooleanFormField.java delete mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/Form.java create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/FormFieldWithOptions.java create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/JidMultiFormField.java create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/JidSingleFormField.java create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/ListMultiFormField.java create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/ListSingleFormField.java create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/SingleValueFormField.java create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/TextMultiFormField.java create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/TextSingleFormField.java create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/form/FillableForm.java create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/form/FilledForm.java create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/form/Form.java create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/form/FormReader.java create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/form/FormWriter.java create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/form/package-info.java delete mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/provider/OptionProvider.java delete mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/provider/RequiredProvider.java delete mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/provider/ValueProvider.java create mode 100644 smack-extensions/src/test/java/org/jivesoftware/smackx/xdata/FormFieldTest.java diff --git a/documentation/extensions/index.md b/documentation/extensions/index.md index f84b5c025..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. | 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/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-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/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/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-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 008baa3bd..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. @@ -22,11 +22,13 @@ 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; @@ -49,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; @@ -601,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; @@ -613,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) { @@ -718,12 +721,12 @@ public final class EntityCapsManager extends Manager { SortedSet fs = new TreeSet<>(new Comparator() { @Override public int compare(FormField f1, FormField f2) { - return f1.getVariable().compareTo(f2.getVariable()); + return f1.getFieldName().compareTo(f2.getFieldName()); } }); for (FormField f : extendedInfo.getFields()) { - if (!f.getVariable().equals("FORM_TYPE")) { + if (!f.getFieldName().equals("FORM_TYPE")) { fs.add(f); } } @@ -739,7 +742,7 @@ public final class EntityCapsManager extends Manager { // 3. For each element, append the XML character data, // followed by the '<' character. for (FormField f : fs) { - sb.append(f.getVariable()); + sb.append(f.getFieldName()); sb.append('<'); formFieldValuesToCaps(f.getValues(), sb); } @@ -763,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/filetransfer/FileTransferNegotiator.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/filetransfer/FileTransferNegotiator.java index e1575df8d..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; @@ -359,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/StreamNegotiator.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/filetransfer/StreamNegotiator.java index 2aaf4c73b..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; @@ -80,13 +81,13 @@ public abstract class StreamNegotiator extends Manager { 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); - field.addValue(namespace); + field.setValue(namespace); form.addField(field.build()); - response.setFeatureNegotiationForm(form); + response.setFeatureNegotiationForm(form.build()); return response; } 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/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/MultiUserChat.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChat.java index 0a2ce3060..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; @@ -533,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 { @@ -552,7 +554,7 @@ public class MultiUserChat { */ public void makeInstant() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { - sendConfigurationForm(new Form(DataForm.Type.submit)); + sendConfigurationForm(null); } /** @@ -804,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); } /** @@ -817,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(); } @@ -849,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); } /** @@ -869,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(); } @@ -1247,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); } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatConstants.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatConstants.java new file mode 100644 index 000000000..ffc00a5a9 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatConstants.java @@ -0,0 +1,23 @@ +/** + * + * Copyright 2020 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.muc; + +public class MultiUserChatConstants { + + public static final String NAMESPACE = "http://jabber.org/protocol/muc"; + +} 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/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/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 01787ea03..000000000 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/ConfigureForm.java +++ /dev/null @@ -1,643 +0,0 @@ -/** - * - * Copyright the original author or authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.jivesoftware.smackx.pubsub; - -import java.util.ArrayList; -import java.util.List; - -import org.jivesoftware.smack.util.ParserUtils; - -import org.jivesoftware.smackx.xdata.Form; -import org.jivesoftware.smackx.xdata.FormField; -import org.jivesoftware.smackx.xdata.packet.DataForm; - -/** - * A decorator for a {@link Form} to easily enable reading and updating - * of node configuration. All operations read or update the underlying {@link DataForm}. - * - *

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())); - } - - /** - * 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 30ef27c25..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 */ 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/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 1b9b28a55..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; @@ -255,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/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/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