selectedKeys = new ArrayList<>(myKeyCount);
for (int i = 0; i < myKeyCount; i++) {
SelectionKey selectionKey = pendingSelectionKeys.poll();
diff --git a/smack-core/src/main/java/org/jivesoftware/smack/XMPPConnection.java b/smack-core/src/main/java/org/jivesoftware/smack/XMPPConnection.java
index 0ef97910a..f7543e724 100644
--- a/smack-core/src/main/java/org/jivesoftware/smack/XMPPConnection.java
+++ b/smack-core/src/main/java/org/jivesoftware/smack/XMPPConnection.java
@@ -91,23 +91,24 @@ import org.jxmpp.jid.EntityFullJid;
* Incoming Stanza Listeners
* Most callbacks (listeners, handlers, …) than you can add to a connection come in three different variants:
*
- * - standard
- * - async (asynchronous)
- * - sync (synchronous)
+ * - asynchronous - e.g., {@link #addAsyncStanzaListener(StanzaListener, StanzaFilter)}
+ * - synchronous - e.g., {@link #addSyncStanzaListener(StanzaListener, StanzaFilter)}
+ * - other - e.g., {@link #addStanzaListener(StanzaListener, StanzaFilter)}
*
*
- * Standard callbacks are invoked concurrently, but it is ensured that the same callback is never run concurrently.
- * The callback's identity is used as key for that. The events delivered to the callback preserve the order of the
- * causing events of the connection.
- *
- *
* Asynchronous callbacks are run decoupled from the connections main event loop. Hence a callback triggered by
* stanza B may (appear to) invoked before a callback triggered by stanza A, even though stanza A arrived before B.
*
*
- * Synchronous callbacks are run synchronous to the main event loop of a connection. Hence they are invoked in the
- * exact order of how events happen there, most importantly the arrival order of incoming stanzas. You should only
- * use synchronous callbacks in rare situations.
+ * Synchronous callbacks are invoked concurrently, but it is ensured that the same callback is never run concurrently
+ * and that they are executed in order. That is, if both stanza A and B trigger the same callback, and A arrives before
+ * B, then the callback will be invoked with A first, and then B. Furthermore, those callbacks are not executed within
+ * the main loop. However it is still advisable that those callbacks do not block or only block briefly.
+ *
+ *
+ * Other callbacks are run synchronous to the main event loop of a connection and are executed within the main loop.
+ * This means that if such a callback blocks, the main event loop also blocks, which can easily cause deadlocks.
+ * Therefore, you should avoid using those callbacks unless you know what you are doing.
*
* Stanza Filters
* Stanza filters allow you to define the predicates for which listeners or collectors should be invoked. For more
@@ -409,7 +410,7 @@ public interface XMPPConnection {
boolean removeStanzaListener(StanzaListener stanzaListener);
/**
- * Registers a synchronous stanza listener with this connection. A stanza listener will be invoked only when
+ * Registers a synchronous stanza listener with this connection. A stanza listener will be invoked only when
* an incoming stanza is received. A stanza filter determines which stanzas will be delivered to the listener. If
* the same stanza listener is added again with a different filter, only the new filter will be used.
*
diff --git a/smack-core/src/main/java/org/jivesoftware/smack/debugger/ConsoleDebugger.java b/smack-core/src/main/java/org/jivesoftware/smack/debugger/ConsoleDebugger.java
index b026f7bb3..a8ef6c00a 100644
--- a/smack-core/src/main/java/org/jivesoftware/smack/debugger/ConsoleDebugger.java
+++ b/smack-core/src/main/java/org/jivesoftware/smack/debugger/ConsoleDebugger.java
@@ -35,7 +35,7 @@ import org.jivesoftware.smack.util.ExceptionUtil;
* @author Gaston Dombiak
*/
public class ConsoleDebugger extends AbstractDebugger {
- private final SimpleDateFormat dateFormatter = new SimpleDateFormat("HH:mm:ss");
+ private final SimpleDateFormat dateFormatter = new SimpleDateFormat("HH:mm:ss.S");
public ConsoleDebugger(XMPPConnection connection) {
super(connection);
diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/LazyStringBuilder.java b/smack-core/src/main/java/org/jivesoftware/smack/util/LazyStringBuilder.java
index 752fda1cf..6e0ded134 100644
--- a/smack-core/src/main/java/org/jivesoftware/smack/util/LazyStringBuilder.java
+++ b/smack-core/src/main/java/org/jivesoftware/smack/util/LazyStringBuilder.java
@@ -1,6 +1,6 @@
/**
*
- * Copyright 2014-2019 Florian Schmaus
+ * Copyright 2014-2023 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -24,10 +24,12 @@ public class LazyStringBuilder implements Appendable, CharSequence {
private final List list;
- private String cache;
+ private transient String cache;
+ private int cachedLength = -1;
private void invalidateCache() {
cache = null;
+ cachedLength = -1;
}
public LazyStringBuilder() {
@@ -65,9 +67,10 @@ public class LazyStringBuilder implements Appendable, CharSequence {
@Override
public int length() {
- if (cache != null) {
- return cache.length();
+ if (cachedLength >= 0) {
+ return cachedLength;
}
+
int length = 0;
try {
for (CharSequence csq : list) {
@@ -78,6 +81,8 @@ public class LazyStringBuilder implements Appendable, CharSequence {
StringBuilder sb = safeToStringBuilder();
throw new RuntimeException("The following LazyStringBuilder threw a NullPointerException: " + sb, npe);
}
+
+ cachedLength = length;
return length;
}
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 21b192b4c..3ecb67877 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
@@ -1,6 +1,6 @@
/**
*
- * Copyright 2014-2021 Florian Schmaus
+ * Copyright 2014-2023 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,6 +28,7 @@ import org.jivesoftware.smack.packet.NamedElement;
import org.jivesoftware.smack.packet.XmlElement;
import org.jivesoftware.smack.packet.XmlEnvironment;
+import org.jxmpp.jid.Jid;
import org.jxmpp.util.XmppDateTime;
public class XmlStringBuilder implements Appendable, CharSequence, Element {
@@ -311,6 +312,18 @@ public class XmlStringBuilder implements Appendable, CharSequence, Element {
return attribute(name, String.valueOf(value));
}
+ public XmlStringBuilder jidAttribute(Jid jid) {
+ assert jid != null;
+ return attribute("jid", jid);
+ }
+
+ public XmlStringBuilder optJidAttribute(Jid jid) {
+ if (jid != null) {
+ attribute("jid", jid);
+ }
+ return this;
+ }
+
public XmlStringBuilder optAttribute(String name, String value) {
if (value != null) {
attribute(name, value);
@@ -593,10 +606,49 @@ public class XmlStringBuilder implements Appendable, CharSequence, Element {
return this;
}
+ enum AppendApproach {
+ /**
+ * Simply add the given CharSequence to this builder.
+ */
+ SINGLE,
+
+ /**
+ * If the given CharSequence is a {@link XmlStringBuilder} or {@link LazyStringBuilder}, then copy the
+ * references of the lazy strings parts into this builder. This approach flattens the string builders into one,
+ * yielding a different performance characteristic.
+ */
+ FLAT,
+ }
+
+ private static AppendApproach APPEND_APPROACH = AppendApproach.SINGLE;
+
+ /**
+ * Set the builders approach on how to append new char sequences.
+ *
+ * @param appendApproach the append approach.
+ */
+ public static void setAppendMethod(AppendApproach appendApproach) {
+ Objects.requireNonNull(appendApproach);
+ APPEND_APPROACH = appendApproach;
+ }
+
@Override
public XmlStringBuilder append(CharSequence csq) {
assert csq != null;
- sb.append(csq);
+ switch (APPEND_APPROACH) {
+ case SINGLE:
+ sb.append(csq);
+ break;
+ case FLAT:
+ if (csq instanceof XmlStringBuilder) {
+ sb.append(((XmlStringBuilder) csq).sb);
+ } else if (csq instanceof LazyStringBuilder) {
+ sb.append((LazyStringBuilder) csq);
+ } else {
+ sb.append(csq);
+ }
+ break;
+ }
return this;
}
diff --git a/smack-debug-slf4j/build.gradle b/smack-debug-slf4j/build.gradle
index 9c2272729..35d904a7e 100644
--- a/smack-debug-slf4j/build.gradle
+++ b/smack-debug-slf4j/build.gradle
@@ -5,5 +5,5 @@ Connect your favourite slf4j backend of choice to get output inside of it"""
dependencies {
api project(':smack-core')
- implementation 'org.slf4j:slf4j-api:[1.7,1.8)'
+ implementation 'org.slf4j:slf4j-api:[1.7,2.0)'
}
diff --git a/smack-examples/build.gradle b/smack-examples/build.gradle
new file mode 100644
index 000000000..7592441a5
--- /dev/null
+++ b/smack-examples/build.gradle
@@ -0,0 +1,10 @@
+description = """\
+Examples and test applications for Smack"""
+
+dependencies {
+ // Smack's integration test framework (sintest) depends on
+ // smack-java*-full and since we may want to use parts of sinttest
+ // in smack-examples, we simply depend sinttest.
+ api project(':smack-integration-test')
+ api project(':smack-omemo-signal')
+}
diff --git a/smack-java8-full/src/main/java/org/jivesoftware/smack/full/BoshConnectionTest.java b/smack-examples/src/main/java/org/igniterealtime/smack/examples/BoshConnectionTest.java
similarity index 65%
rename from smack-java8-full/src/main/java/org/jivesoftware/smack/full/BoshConnectionTest.java
rename to smack-examples/src/main/java/org/igniterealtime/smack/examples/BoshConnectionTest.java
index 4063c72ff..3d18c7365 100644
--- a/smack-java8-full/src/main/java/org/jivesoftware/smack/full/BoshConnectionTest.java
+++ b/smack-examples/src/main/java/org/igniterealtime/smack/examples/BoshConnectionTest.java
@@ -2,19 +2,23 @@
*
* Copyright 2022 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
+ * This file is part of smack-examples.
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * smack-examples is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
*
- * 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.
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
-package org.jivesoftware.smack.full;
+package org.igniterealtime.smack.examples;
import java.io.IOException;
diff --git a/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/DoX.java b/smack-examples/src/main/java/org/igniterealtime/smack/examples/DoX.java
similarity index 94%
rename from smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/DoX.java
rename to smack-examples/src/main/java/org/igniterealtime/smack/examples/DoX.java
index 093a5aea9..cd3745437 100644
--- a/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/DoX.java
+++ b/smack-examples/src/main/java/org/igniterealtime/smack/examples/DoX.java
@@ -2,9 +2,9 @@
*
* Copyright 2019 Florian Schmaus
*
- * This file is part of smack-repl.
+ * This file is part of smack-examples.
*
- * smack-repl is free software; you can redistribute it and/or modify
+ * smack-examples is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
@@ -18,7 +18,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
-package org.igniterealtime.smack.smackrepl;
+package org.igniterealtime.smack.examples;
import java.io.IOException;
diff --git a/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/IoT.java b/smack-examples/src/main/java/org/igniterealtime/smack/examples/IoT.java
similarity index 98%
rename from smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/IoT.java
rename to smack-examples/src/main/java/org/igniterealtime/smack/examples/IoT.java
index 66a968c31..12c74fb2a 100644
--- a/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/IoT.java
+++ b/smack-examples/src/main/java/org/igniterealtime/smack/examples/IoT.java
@@ -2,9 +2,9 @@
*
* Copyright 2016 Florian Schmaus
*
- * This file is part of smack-repl.
+ * This file is part of smack-examples.
*
- * smack-repl is free software; you can redistribute it and/or modify
+ * smack-examples is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
@@ -18,7 +18,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
-package org.igniterealtime.smack.smackrepl;
+package org.igniterealtime.smack.examples;
import java.util.Collections;
import java.util.List;
diff --git a/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/Nio.java b/smack-examples/src/main/java/org/igniterealtime/smack/examples/Nio.java
similarity index 96%
rename from smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/Nio.java
rename to smack-examples/src/main/java/org/igniterealtime/smack/examples/Nio.java
index 09d296bb5..81ef164cb 100644
--- a/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/Nio.java
+++ b/smack-examples/src/main/java/org/igniterealtime/smack/examples/Nio.java
@@ -2,9 +2,9 @@
*
* Copyright 2018-2021 Florian Schmaus
*
- * This file is part of smack-repl.
+ * This file is part of smack-examples.
*
- * smack-repl is free software; you can redistribute it and/or modify
+ * smack-examples is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
@@ -18,7 +18,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
-package org.igniterealtime.smack.smackrepl;
+package org.igniterealtime.smack.examples;
import java.io.BufferedWriter;
import java.io.IOException;
diff --git a/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/OmemoClient.java b/smack-examples/src/main/java/org/igniterealtime/smack/examples/OmemoClient.java
similarity index 98%
rename from smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/OmemoClient.java
rename to smack-examples/src/main/java/org/igniterealtime/smack/examples/OmemoClient.java
index dc4b343e6..b247c77b2 100644
--- a/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/OmemoClient.java
+++ b/smack-examples/src/main/java/org/igniterealtime/smack/examples/OmemoClient.java
@@ -2,9 +2,9 @@
*
* Copyright 2019 Paul Schaub
*
- * This file is part of smack-repl.
+ * This file is part of smack-examples.
*
- * smack-repl is free software; you can redistribute it and/or modify
+ * smack-examples is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
@@ -18,7 +18,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
-package org.igniterealtime.smack.smackrepl;
+package org.igniterealtime.smack.examples;
import java.io.IOException;
import java.nio.file.Files;
diff --git a/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/TlsTest.java b/smack-examples/src/main/java/org/igniterealtime/smack/examples/TlsTest.java
similarity index 96%
rename from smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/TlsTest.java
rename to smack-examples/src/main/java/org/igniterealtime/smack/examples/TlsTest.java
index 08bd25b6d..0e924fa9a 100644
--- a/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/TlsTest.java
+++ b/smack-examples/src/main/java/org/igniterealtime/smack/examples/TlsTest.java
@@ -2,9 +2,9 @@
*
* Copyright 2016 Florian Schmaus
*
- * This file is part of smack-repl.
+ * This file is part of smack-examples.
*
- * smack-repl is free software; you can redistribute it and/or modify
+ * smack-examples is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
@@ -18,7 +18,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
-package org.igniterealtime.smack.smackrepl;
+package org.igniterealtime.smack.examples;
import java.io.IOException;
import java.security.KeyManagementException;
diff --git a/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/WebSocketConnection.java b/smack-examples/src/main/java/org/igniterealtime/smack/examples/WebSocketConnection.java
similarity index 95%
rename from smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/WebSocketConnection.java
rename to smack-examples/src/main/java/org/igniterealtime/smack/examples/WebSocketConnection.java
index f693fb739..9e4e5856f 100644
--- a/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/WebSocketConnection.java
+++ b/smack-examples/src/main/java/org/igniterealtime/smack/examples/WebSocketConnection.java
@@ -2,9 +2,9 @@
*
* Copyright 2021 Florian Schmaus
*
- * This file is part of smack-repl.
+ * This file is part of smack-examples.
*
- * smack-repl is free software; you can redistribute it and/or modify
+ * smack-examples is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
@@ -18,7 +18,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
-package org.igniterealtime.smack.smackrepl;
+package org.igniterealtime.smack.examples;
import java.io.IOException;
import java.net.URISyntaxException;
diff --git a/smack-examples/src/main/java/org/igniterealtime/smack/examples/XmlStringBuilderTest.java b/smack-examples/src/main/java/org/igniterealtime/smack/examples/XmlStringBuilderTest.java
new file mode 100644
index 000000000..b02e5cf81
--- /dev/null
+++ b/smack-examples/src/main/java/org/igniterealtime/smack/examples/XmlStringBuilderTest.java
@@ -0,0 +1,118 @@
+/**
+ *
+ * Copyright 2023 Florian Schmaus
+ *
+ * This file is part of smack-examples.
+ *
+ * smack-examples is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+package org.igniterealtime.smack.examples;
+
+import java.util.function.Supplier;
+
+import org.jivesoftware.smack.util.XmlStringBuilder;
+
+public class XmlStringBuilderTest {
+ static int COUNT_OUTER = 500;
+ static int COUNT_INNER = 50;
+
+ public static void main(String[] args) throws Exception {
+ test1();
+ test2();
+ test3();
+ }
+
+ public static void test1() throws Exception {
+ // CHECKSTYLE:OFF
+ System.out.println("Test 1");
+ // CHECKSTYLE:ON
+ XmlStringBuilder parent = new XmlStringBuilder();
+ XmlStringBuilder child = new XmlStringBuilder();
+ XmlStringBuilder child2 = new XmlStringBuilder();
+
+ for (int i = 1; i < COUNT_OUTER; i++) {
+ XmlStringBuilder cs = new XmlStringBuilder();
+ for (int j = 0; j < COUNT_INNER; j++) {
+ cs.append("abc");
+ }
+ child2.append((CharSequence) cs);
+ }
+
+ child.append((CharSequence) child2);
+ parent.append((CharSequence) child);
+
+ time("test1: parent", () -> "len=" + parent.toString().length());
+ time("test1: child", () -> "len=" + child.toString().length());
+ time("test1: child2", () -> "len=" + child2.toString().length());
+ }
+
+ public static void test2() throws Exception {
+ // CHECKSTYLE:OFF
+ System.out.println("Test 2: evaluate children first");
+ // CHECKSTYLE:ON
+ XmlStringBuilder parent = new XmlStringBuilder();
+ XmlStringBuilder child = new XmlStringBuilder();
+ XmlStringBuilder child2 = new XmlStringBuilder();
+
+ for (int i = 1; i < COUNT_OUTER; i++) {
+ XmlStringBuilder cs = new XmlStringBuilder();
+ for (int j = 0; j < COUNT_INNER; j++) {
+ cs.append("abc");
+ }
+ child2.append((CharSequence) cs);
+ }
+
+ child.append((CharSequence) child2);
+ parent.append((CharSequence) child);
+
+ time("test2: child2", () -> "len=" + child2.toString().length());
+ time("test2: child", () -> "len=" + child.toString().length());
+ time("test2: parent", () -> "len=" + parent.toString().length());
+ }
+
+ public static void test3() throws Exception {
+ // CHECKSTYLE:OFF
+ System.out.println("Test 3: use append(XmlStringBuilder)");
+ // CHECKSTYLE:ON
+ XmlStringBuilder parent = new XmlStringBuilder();
+ XmlStringBuilder child = new XmlStringBuilder();
+ XmlStringBuilder child2 = new XmlStringBuilder();
+
+ for (int i = 1; i < COUNT_OUTER; i++) {
+ XmlStringBuilder cs = new XmlStringBuilder();
+ for (int j = 0; j < COUNT_INNER; j++) {
+ cs.append("abc");
+ }
+ child2.append(cs);
+ }
+
+ child.append(child2);
+ parent.append(child);
+
+ time("test3: parent", () -> "len=" + parent.toString().length());
+ time("test3: child", () -> "len=" + child.toString().length());
+ time("test3: child2", () -> "len=" + child2.toString().length());
+ }
+
+ static void time(String name, Supplier block) {
+ long start = System.currentTimeMillis();
+ String result = block.get();
+ long end = System.currentTimeMillis();
+
+ // CHECKSTYLE:OFF
+ System.out.println(name + " took " + (end - start) + "ms: " + result);
+ // CHECKSTYLE:ONy
+ }
+}
diff --git a/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/XmppTools.java b/smack-examples/src/main/java/org/igniterealtime/smack/examples/XmppTools.java
similarity index 97%
rename from smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/XmppTools.java
rename to smack-examples/src/main/java/org/igniterealtime/smack/examples/XmppTools.java
index eeb6c7a40..f262102c2 100644
--- a/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/XmppTools.java
+++ b/smack-examples/src/main/java/org/igniterealtime/smack/examples/XmppTools.java
@@ -2,9 +2,9 @@
*
* Copyright 2016-2021 Florian Schmaus
*
- * This file is part of smack-repl.
+ * This file is part of smack-examples.
*
- * smack-repl is free software; you can redistribute it and/or modify
+ * smack-examples is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
@@ -18,7 +18,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
-package org.igniterealtime.smack.smackrepl;
+package org.igniterealtime.smack.examples;
import java.io.IOException;
import java.security.KeyManagementException;
diff --git a/smack-examples/src/main/java/org/igniterealtime/smack/examples/package-info.java b/smack-examples/src/main/java/org/igniterealtime/smack/examples/package-info.java
new file mode 100644
index 000000000..ed3cf17e7
--- /dev/null
+++ b/smack-examples/src/main/java/org/igniterealtime/smack/examples/package-info.java
@@ -0,0 +1,25 @@
+/**
+ *
+ * Copyright 2023 Florian Schmaus
+ *
+ * This file is part of smack-examples.
+ *
+ * smack-examples is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * Examples and tests for Smack.
+ */
+package org.igniterealtime.smack.examples;
diff --git a/smack-examples/src/test/java/org/igniterealtime/smack/examples/SmackExamplesTest.java b/smack-examples/src/test/java/org/igniterealtime/smack/examples/SmackExamplesTest.java
new file mode 100644
index 000000000..35aa7190d
--- /dev/null
+++ b/smack-examples/src/test/java/org/igniterealtime/smack/examples/SmackExamplesTest.java
@@ -0,0 +1,33 @@
+/**
+ *
+ * Copyright 2023 Florian Schmaus
+ *
+ * This file is part of smack-examples.
+ *
+ * smack-examples is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+package org.igniterealtime.smack.examples;
+
+import org.junit.jupiter.api.Test;
+
+public class SmackExamplesTest {
+ /**
+ * Just here to ensure jacoco is not complaining.
+ */
+ @Test
+ public void emptyTest() {
+ }
+
+}
diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/chat_markers/ChatMarkersManager.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/chat_markers/ChatMarkersManager.java
index b2f99bd76..60b125742 100644
--- a/smack-experimental/src/main/java/org/jivesoftware/smackx/chat_markers/ChatMarkersManager.java
+++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/chat_markers/ChatMarkersManager.java
@@ -174,7 +174,10 @@ public final class ChatMarkersManager extends Manager {
* @throws XMPPErrorException in case an error response was received.
* @throws NoResponseException if no response was received.
* @throws InterruptedException if the connection is interrupted.
+ * @deprecated This method serves no purpose, as servers do not announce this feature.
*/
+ // TODO: Remove in Smack 4.6.
+ @Deprecated
public boolean isSupportedByServer()
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
return ServiceDiscoveryManager.getInstanceFor(connection())
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 94dfc2f07..985ca8b48 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
@@ -1,6 +1,6 @@
/**
*
- * Copyright © 2017-2020 Florian Schmaus, 2016-2017 Fernando Ramirez
+ * Copyright © 2017-2023 Florian Schmaus, 2016-2017 Fernando Ramirez
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -44,8 +44,8 @@ import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smack.util.Objects;
import org.jivesoftware.smack.util.StringUtils;
+import org.jivesoftware.smackx.commands.AdHocCommand;
import org.jivesoftware.smackx.commands.AdHocCommandManager;
-import org.jivesoftware.smackx.commands.RemoteCommand;
import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
import org.jivesoftware.smackx.disco.packet.DiscoverItems;
@@ -233,7 +233,7 @@ public final class MamManager extends Manager {
super(connection);
this.archiveAddress = archiveAddress;
serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection);
- adHocCommandManager = AdHocCommandManager.getAddHocCommandsManager(connection);
+ adHocCommandManager = AdHocCommandManager.getInstance(connection);
}
/**
@@ -759,7 +759,7 @@ public final class MamManager extends Manager {
return false;
}
- public RemoteCommand getAdvancedConfigurationCommand() throws InterruptedException, XMPPException, SmackException {
+ public AdHocCommand getAdvancedConfigurationCommand() throws InterruptedException, XMPPException, SmackException {
DiscoverItems discoverItems = adHocCommandManager.discoverCommands(archiveAddress);
for (DiscoverItems.Item item : discoverItems.getItems()) {
if (item.getNode().equals(ADVANCED_CONFIG_NODE))
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 78ce687a6..9956533f7 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-2020 Florian Schmaus
+ * Copyright 2016-2023 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -27,8 +27,9 @@ import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException.XMPPErrorException;
+import org.jivesoftware.smackx.commands.AdHocCommand;
import org.jivesoftware.smackx.commands.AdHocCommandManager;
-import org.jivesoftware.smackx.commands.RemoteCommand;
+import org.jivesoftware.smackx.commands.AdHocCommandResult;
import org.jivesoftware.smackx.xdata.form.FillableForm;
import org.jxmpp.jid.EntityBareJid;
@@ -56,37 +57,38 @@ public class ServiceAdministrationManager extends Manager {
public ServiceAdministrationManager(XMPPConnection connection) {
super(connection);
- adHocCommandManager = AdHocCommandManager.getAddHocCommandsManager(connection);
+ adHocCommandManager = AdHocCommandManager.getInstance(connection);
}
- public RemoteCommand addUser() {
+ public AdHocCommand addUser() {
return addUser(connection().getXMPPServiceDomain());
}
- public RemoteCommand addUser(Jid service) {
+ public AdHocCommand addUser(Jid service) {
return adHocCommandManager.getRemoteCommand(service, COMMAND_NODE_HASHSIGN + "add-user");
}
public void addUser(final EntityBareJid userJid, final String password)
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
- RemoteCommand command = addUser();
- command.execute();
+ AdHocCommand command = addUser();
- FillableForm answerForm = new FillableForm(command.getForm());
+ AdHocCommandResult.StatusExecuting commandExecutingResult = command.execute().asExecutingOrThrow();
+
+ FillableForm answerForm = commandExecutingResult.getFillableForm();
answerForm.setAnswer("accountjid", userJid);
answerForm.setAnswer("password", password);
answerForm.setAnswer("password-verify", password);
- command.execute(answerForm);
- assert command.isCompleted();
+ AdHocCommandResult result = command.execute(answerForm);
+ assert result.isCompleted();
}
- public RemoteCommand deleteUser() {
+ public AdHocCommand deleteUser() {
return deleteUser(connection().getXMPPServiceDomain());
}
- public RemoteCommand deleteUser(Jid service) {
+ public AdHocCommand deleteUser(Jid service) {
return adHocCommandManager.getRemoteCommand(service, COMMAND_NODE_HASHSIGN + "delete-user");
}
@@ -98,14 +100,14 @@ public class ServiceAdministrationManager extends Manager {
public void deleteUser(Set jidsToDelete)
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
- RemoteCommand command = deleteUser();
- command.execute();
+ AdHocCommand command = deleteUser();
+ AdHocCommandResult.StatusExecuting commandExecutingResult = command.execute().asExecutingOrThrow();
- FillableForm answerForm = new FillableForm(command.getForm());
+ FillableForm answerForm = commandExecutingResult.getFillableForm();
answerForm.setAnswer("accountjids", jidsToDelete);
- command.execute(answerForm);
- assert command.isCompleted();
+ AdHocCommandResult result = command.execute(answerForm);
+ assert result.isCompleted();
}
}
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 c549a152b..8bd738fa2 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
@@ -388,7 +388,10 @@ public final class EntityCapsManager extends Manager {
if (autoEnableEntityCaps)
enableEntityCaps();
- connection.addAsyncStanzaListener(new StanzaListener() {
+ // Note that this is a *synchronous* stanza listener to avoid unnecessary feature lookups. If this were to be an
+ // asynchronous listener, then it would be possible that the entity caps information was not processed when the
+ // features of entity are looked up. See SMACK-937.
+ connection.addStanzaListener(new StanzaListener() {
// Listen for remote presence stanzas with the caps extension
// If we receive such a stanza, record the JID and nodeVer
@Override
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/AbstractAdHocCommand.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/AbstractAdHocCommand.java
new file mode 100755
index 000000000..8a12b29f3
--- /dev/null
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/AbstractAdHocCommand.java
@@ -0,0 +1,277 @@
+/**
+ *
+ * Copyright 2005-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.commands;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import org.jivesoftware.smack.SmackException.NoResponseException;
+import org.jivesoftware.smack.SmackException.NotConnectedException;
+import org.jivesoftware.smack.XMPPException.XMPPErrorException;
+import org.jivesoftware.smack.packet.StanzaError;
+import org.jivesoftware.smack.util.StringUtils;
+import org.jivesoftware.smackx.commands.packet.AdHocCommandData;
+import org.jivesoftware.smackx.commands.packet.AdHocCommandData.Action;
+import org.jivesoftware.smackx.commands.packet.AdHocCommandData.AllowedAction;
+import org.jivesoftware.smackx.commands.packet.AdHocCommandData.Status;
+
+/**
+ * An ad-hoc command is responsible for executing the provided service and
+ * storing the result of the execution. Each new request will create a new
+ * instance of the command, allowing information related to executions to be
+ * stored in it. For example suppose that a command that retrieves the list of
+ * users on a server is implemented. When the command is executed it gets that
+ * list and the result is stored as a form in the command instance, i.e. the
+ * getForm
method retrieves a form with all the users.
+ *
+ * Each command has a node
that should be unique within a given JID.
+ *
+ *
+ * Commands may have zero or more stages. Each stage is usually used for
+ * gathering information required for the command execution. Users are able to
+ * move forward or backward across the different stages. Commands may not be
+ * cancelled while they are being executed. However, users may request the
+ * "cancel" action when submitting a stage response indicating that the command
+ * execution should be aborted. Thus, releasing any collected information.
+ * Commands that require user interaction (i.e. have more than one stage) will
+ * have to provide the data forms the user must complete in each stage and the
+ * allowed actions the user might perform during each stage (e.g. go to the
+ * previous stage or go to the next stage).
+ *
+ * All the actions may throw an XMPPException if there is a problem executing
+ * them. The XMPPError
of that exception may have some specific
+ * information about the problem. The possible extensions are:
+ *
+ * - malformed-action. Extension of a bad-request error.
+ * - bad-action. Extension of a bad-request error.
+ * - bad-locale. Extension of a bad-request error.
+ * - bad-payload. Extension of a bad-request error.
+ * - bad-sessionid. Extension of a bad-request error.
+ * - session-expired. Extension of a not-allowed error.
+ *
+ *
+ * See the SpecificErrorCondition
class for detailed description
+ * of each one.
+ *
+ * Use the getSpecificErrorConditionFrom
to obtain the specific
+ * information from an XMPPError
.
+ *
+ * @author Gabriel Guardincerri
+ * @author Florian Schmaus
+ *
+ */
+public abstract class AbstractAdHocCommand {
+ private final List requests = new ArrayList<>();
+ private final List results = new ArrayList<>();
+
+ private final String node;
+
+ private final String name;
+
+ /**
+ * The session ID of this execution.
+ */
+ private String sessionId;
+
+ protected AbstractAdHocCommand(String node, String name) {
+ this.node = StringUtils.requireNotNullNorEmpty(node, "Ad-Hoc command node must be given");
+ this.name = name;
+ }
+
+ protected AbstractAdHocCommand(String node) {
+ this(node, null);
+ }
+
+ void addRequest(AdHocCommandData request) {
+ requests.add(request);
+ }
+
+ void addResult(AdHocCommandResult result) {
+ results.add(result);
+ }
+
+ /**
+ * Returns the specific condition of the error
or null
if the
+ * error doesn't have any.
+ *
+ * @param error the error the get the specific condition from.
+ * @return the specific condition of this error, or null if it doesn't have
+ * any.
+ */
+ public static SpecificErrorCondition getSpecificErrorCondition(StanzaError error) {
+ // This method is implemented to provide an easy way of getting a packet
+ // extension of the XMPPError.
+ for (SpecificErrorCondition condition : SpecificErrorCondition.values()) {
+ if (error.getExtension(condition.toString(),
+ AdHocCommandData.SpecificError.namespace) != null) {
+ return condition;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the human readable name of the command.
+ *
+ * @return the human readable name of the command
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the unique identifier of the command. It is unique for the
+ * OwnerJID
.
+ *
+ * @return the unique identifier of the command.
+ */
+ public String getNode() {
+ return node;
+ }
+
+ public String getSessionId() {
+ return sessionId;
+ }
+
+ protected void setSessionId(String sessionId) {
+ assert this.sessionId == null || this.sessionId.equals(sessionId);
+ this.sessionId = StringUtils.requireNotNullNorEmpty(sessionId, "Must provide a session ID");
+ }
+
+ public AdHocCommandData getLastRequest() {
+ if (requests.isEmpty()) return null;
+ return requests.get(requests.size() - 1);
+ }
+
+ public AdHocCommandResult getLastResult() {
+ if (results.isEmpty()) return null;
+ return results.get(results.size() - 1);
+ }
+
+ /**
+ * Returns the notes that the command has at the current stage.
+ *
+ * @return a list of notes.
+ */
+ public List getNotes() {
+ AdHocCommandResult result = getLastResult();
+ if (result == null) return null;
+
+ return result.getResponse().getNotes();
+ }
+
+ /**
+ * Cancels the execution of the command. This can be invoked on any stage of
+ * the execution. If there is a problem executing the command it throws an
+ * XMPPException.
+ *
+ * @throws NoResponseException if there was no response from the remote entity.
+ * @throws XMPPErrorException if there is a problem executing the command.
+ * @throws NotConnectedException if the XMPP connection is not connected.
+ * @throws InterruptedException if the calling thread was interrupted.
+ */
+ public abstract void cancel() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException;
+
+ /**
+ * Returns a collection with the allowed actions based on the current stage.
+ * Possible actions are: {@link AllowedAction#prev prev}, {@link AllowedAction#next next} and
+ * {@link AllowedAction#complete complete}. This method will be only invoked for commands that
+ * have one or more stages.
+ *
+ * @return a collection with the allowed actions based on the current stage
+ * as defined in the SessionData.
+ */
+ public final Set getActions() {
+ AdHocCommandResult result = getLastResult();
+ if (result == null) return null;
+
+ return result.getResponse().getActions();
+ }
+
+ /**
+ * Returns the action available for the current stage which is
+ * considered the equivalent to "execute". When the requester sends his
+ * reply, if no action was defined in the command then the action will be
+ * assumed "execute" thus assuming the action returned by this method. This
+ * method will never be invoked for commands that have no stages.
+ *
+ * @return the action available for the current stage which is considered
+ * the equivalent to "execute".
+ */
+ protected AllowedAction getExecuteAction() {
+ AdHocCommandResult result = getLastResult();
+ if (result == null) return null;
+
+ return result.getResponse().getExecuteAction();
+ }
+
+ /**
+ * Returns the status of the current stage.
+ *
+ * @return the current status.
+ */
+ public Status getStatus() {
+ AdHocCommandResult result = getLastResult();
+ if (result == null) return null;
+
+ return result.getResponse().getStatus();
+ }
+
+ /**
+ * Check if this command has been completed successfully.
+ *
+ * @return true
if this command is completed.
+ * @since 4.2
+ */
+ public boolean isCompleted() {
+ return getStatus() == AdHocCommandData.Status.completed;
+ }
+
+ /**
+ * Returns true if the action
is available in the current stage.
+ * The {@link Action#cancel cancel} action is always allowed. To define the
+ * available actions use the addActionAvailable
method.
+ *
+ * @param action The action to check if it is available.
+ * @return True if the action is available for the current stage.
+ */
+ public final boolean isValidAction(Action action) {
+ if (action == Action.cancel) {
+ return true;
+ }
+
+ final AllowedAction executeAction;
+ if (action == Action.execute) {
+ AdHocCommandResult result = getLastResult();
+ executeAction = result.getResponse().getExecuteAction();
+
+ // This is basically the case that was clarified with
+ // https://github.com/xsf/xeps/commit/fdaee2da8ffd34b5b5151e90ef1df8b396a06531 and
+ // https://github.com/xsf/xeps/pull/591.
+ if (executeAction == null) {
+ return false;
+ }
+ } else {
+ executeAction = action.allowedAction;
+ assert executeAction != null;
+ }
+
+ Set actions = getActions();
+ return actions != null && actions.contains(executeAction);
+ }
+}
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 347c08f69..0e7fabaaa 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
@@ -16,193 +16,68 @@
*/
package org.jivesoftware.smackx.commands;
-import java.util.List;
-
import org.jivesoftware.smack.SmackException.NoResponseException;
import org.jivesoftware.smack.SmackException.NotConnectedException;
+import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException.XMPPErrorException;
-import org.jivesoftware.smack.packet.StanzaError;
-
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.util.Objects;
import org.jivesoftware.smackx.commands.packet.AdHocCommandData;
import org.jivesoftware.smackx.xdata.form.FillableForm;
+import org.jivesoftware.smackx.xdata.form.SubmitForm;
import org.jivesoftware.smackx.xdata.packet.DataForm;
import org.jxmpp.jid.Jid;
/**
- * An ad-hoc command is responsible for executing the provided service and
- * storing the result of the execution. Each new request will create a new
- * instance of the command, allowing information related to executions to be
- * stored in it. For example suppose that a command that retrieves the list of
- * users on a server is implemented. When the command is executed it gets that
- * list and the result is stored as a form in the command instance, i.e. the
- * getForm
method retrieves a form with all the users.
- *
- * Each command has a node
that should be unique within a given JID.
- *
- *
- * Commands may have zero or more stages. Each stage is usually used for
- * gathering information required for the command execution. Users are able to
- * move forward or backward across the different stages. Commands may not be
- * cancelled while they are being executed. However, users may request the
- * "cancel" action when submitting a stage response indicating that the command
- * execution should be aborted. Thus, releasing any collected information.
- * Commands that require user interaction (i.e. have more than one stage) will
- * have to provide the data forms the user must complete in each stage and the
- * allowed actions the user might perform during each stage (e.g. go to the
- * previous stage or go to the next stage).
- *
- * All the actions may throw an XMPPException if there is a problem executing
- * them. The XMPPError
of that exception may have some specific
- * information about the problem. The possible extensions are:
- *
- * - malformed-action. Extension of a bad-request error.
- * - bad-action. Extension of a bad-request error.
- * - bad-locale. Extension of a bad-request error.
- * - bad-payload. Extension of a bad-request error.
- * - bad-sessionid. Extension of a bad-request error.
- * - session-expired. Extension of a not-allowed error.
- *
- *
- * See the SpecificErrorCondition
class for detailed description
- * of each one.
- *
- * Use the getSpecificErrorConditionFrom
to obtain the specific
- * information from an XMPPError
.
+ * Represents a ad-hoc command invoked on a remote entity. Invoking one of the
+ * {@link #execute()}, {@link #next(SubmitForm)},
+ * {@link #prev()}, {@link #cancel()} or
+ * {@link #complete(SubmitForm)} actions results in executing that
+ * action on the remote entity. In response to that action the internal state
+ * of the this command instance will change. For example, if the command is a
+ * single stage command, then invoking the execute action will execute this
+ * action in the remote location. After that the local instance will have a
+ * state of "completed" and a form or notes that applies.
*
* @author Gabriel Guardincerri
+ * @author Florian Schmaus
*
*/
-public abstract class AdHocCommand {
- // TODO: Analyze the redesign of command by having an ExecutionResponse as a
- // TODO: result to the execution of every action. That result should have all the
- // TODO: information related to the execution, e.g. the form to fill. Maybe this
- // TODO: design is more intuitive and simpler than the current one that has all in
- // TODO: one class.
-
- private AdHocCommandData data;
-
- public AdHocCommand() {
- super();
- data = new AdHocCommandData();
- }
+public class AdHocCommand extends AbstractAdHocCommand {
/**
- * Returns the specific condition of the error
or null
if the
- * error doesn't have any.
- *
- * @param error the error the get the specific condition from.
- * @return the specific condition of this error, or null if it doesn't have
- * any.
+ * The connection that is used to execute this command
*/
- public static SpecificErrorCondition getSpecificErrorCondition(StanzaError error) {
- // This method is implemented to provide an easy way of getting a packet
- // extension of the XMPPError.
- for (SpecificErrorCondition condition : SpecificErrorCondition.values()) {
- if (error.getExtension(condition.toString(),
- AdHocCommandData.SpecificError.namespace) != null) {
- return condition;
- }
- }
- return null;
- }
+ private final XMPPConnection connection;
/**
- * Set the human readable name of the command, usually used for
- * displaying in a UI.
- *
- * @param name the name.
+ * The full JID of the command host
*/
- public void setName(String name) {
- data.setName(name);
- }
+ private final Jid jid;
/**
- * Returns the human readable name of the command.
+ * Creates a new RemoteCommand that uses an specific connection to execute a
+ * command identified by node
in the host identified by
+ * jid
*
- * @return the human readable name of the command
+ * @param connection the connection to use for the execution.
+ * @param node the identifier of the command.
+ * @param jid the JID of the host.
*/
- public String getName() {
- return data.getName();
+ protected AdHocCommand(XMPPConnection connection, String node, Jid jid) {
+ super(node);
+ this.connection = Objects.requireNonNull(connection);
+ this.jid = Objects.requireNonNull(jid);
}
- /**
- * Sets the unique identifier of the command. This value must be unique for
- * the OwnerJID
.
- *
- * @param node the unique identifier of the command.
- */
- public void setNode(String node) {
- data.setNode(node);
+ public Jid getOwnerJID() {
+ return jid;
}
- /**
- * Returns the unique identifier of the command. It is unique for the
- * OwnerJID
.
- *
- * @return the unique identifier of the command.
- */
- public String getNode() {
- return data.getNode();
- }
-
- /**
- * Returns the full JID of the owner of this command. This JID is the "to" of a
- * execution request.
- *
- * @return the owner JID.
- */
- public abstract Jid getOwnerJID();
-
- /**
- * Returns the notes that the command has at the current stage.
- *
- * @return a list of notes.
- */
- public List getNotes() {
- return data.getNotes();
- }
-
- /**
- * Adds a note to the current stage. This should be used when setting a
- * response to the execution of an action. All the notes added here are
- * returned by the {@link #getNotes} method during the current stage.
- * Once the stage changes all the notes are discarded.
- *
- * @param note the note.
- */
- protected void addNote(AdHocCommandNote note) {
- data.addNote(note);
- }
-
- public String getRaw() {
- return data.getChildElementXML().toString();
- }
-
- /**
- * Returns the form of the current stage. Usually it is the form that must
- * be answered to execute the next action. If that is the case it should be
- * used by the requester to fill all the information that the executor needs
- * to continue to the next stage. It can also be the result of the
- * execution.
- *
- * @return the form of the current stage to fill out or the result of the
- * execution.
- */
- public DataForm getForm() {
- return data.getForm();
- }
-
- /**
- * Sets the form of the current stage. This should be used when setting a
- * response. It could be a form to fill out the information needed to go to
- * the next stage or the result of an execution.
- *
- * @param form the form of the current stage to fill out or the result of the
- * execution.
- */
- protected void setForm(DataForm form) {
- data.setForm(form);
+ @Override
+ public final void cancel() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
+ executeAction(AdHocCommandData.Action.cancel);
}
/**
@@ -210,12 +85,15 @@ public abstract class AdHocCommand {
* command. It is invoked on every command. If there is a problem executing
* the command it throws an XMPPException.
*
+ * @return an ad-hoc command result.
* @throws NoResponseException if there was no response from the remote entity.
* @throws XMPPErrorException if there is an error executing the command.
* @throws NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted.
*/
- public abstract void execute() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException;
+ public final AdHocCommandResult execute() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
+ return executeAction(AdHocCommandData.Action.execute);
+ }
/**
* Executes the next action of the command with the information provided in
@@ -224,13 +102,16 @@ public abstract class AdHocCommand {
* or more stages. If there is a problem executing the command it throws an
* XMPPException.
*
- * @param response the form answer of the previous stage.
+ * @param filledForm the form answer of the previous stage.
+ * @return an ad-hoc command result.
* @throws NoResponseException if there was no response from the remote entity.
* @throws XMPPErrorException if there is a problem executing the command.
* @throws NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted.
*/
- public abstract void next(FillableForm response) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException;
+ public final AdHocCommandResult next(SubmitForm filledForm) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
+ return executeAction(AdHocCommandData.Action.next, filledForm.getDataForm());
+ }
/**
* Completes the command execution with the information provided in the
@@ -239,14 +120,16 @@ public abstract class AdHocCommand {
* or more stages. If there is a problem executing the command it throws an
* XMPPException.
*
- * @param response the form answer of the previous stage.
- *
+ * @param filledForm the form answer of the previous stage.
+ * @return an ad-hoc command result.
* @throws NoResponseException if there was no response from the remote entity.
* @throws XMPPErrorException if there is a problem executing the command.
* @throws NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted.
*/
- public abstract void complete(FillableForm response) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException;
+ public AdHocCommandResult complete(SubmitForm filledForm) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
+ return executeAction(AdHocCommandData.Action.complete, filledForm.getDataForm());
+ }
/**
* Goes to the previous stage. The requester is asking to re-send the
@@ -254,224 +137,70 @@ public abstract class AdHocCommand {
* the previous one. If there is a problem executing the command it throws
* an XMPPException.
*
+ * @return an ad-hoc command result.
* @throws NoResponseException if there was no response from the remote entity.
* @throws XMPPErrorException if there is a problem executing the command.
* @throws NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted.
*/
- public abstract void prev() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException;
+ public final AdHocCommandResult prev() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
+ return executeAction(AdHocCommandData.Action.prev);
+ }
/**
- * Cancels the execution of the command. This can be invoked on any stage of
- * the execution. If there is a problem executing the command it throws an
- * XMPPException.
+ * Executes the default action of the command with the information provided
+ * in the Form. This form must be the answer form of the previous stage. If
+ * there is a problem executing the command it throws an XMPPException.
*
- * @throws NoResponseException if there was no response from the remote entity.
- * @throws XMPPErrorException if there is a problem executing the command.
+ * @param form the form answer of the previous stage.
+ * @return an ad-hoc command result.
+ * @throws XMPPErrorException if an error occurs.
+ * @throws NoResponseException if there was no response from the server.
* @throws NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted.
*/
- public abstract void cancel() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException;
+ public final AdHocCommandResult execute(FillableForm form) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
+ return executeAction(AdHocCommandData.Action.execute, form.getDataFormToSubmit());
+ }
+
+ private AdHocCommandResult executeAction(AdHocCommandData.Action action) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
+ return executeAction(action, null);
+ }
/**
- * Returns a collection with the allowed actions based on the current stage.
- * Possible actions are: {@link Action#prev prev}, {@link Action#next next} and
- * {@link Action#complete complete}. This method will be only invoked for commands that
- * have one or more stages.
+ * Executes the action
with the form
.
+ * The action could be any of the available actions. The form must
+ * be the answer of the previous stage. It can be null
if it is the first stage.
*
- * @return a collection with the allowed actions based on the current stage
- * as defined in the SessionData.
+ * @param action the action to execute.
+ * @param form the form with the information.
+ * @throws XMPPErrorException if there is a problem executing the command.
+ * @throws NoResponseException if there was no response from the server.
+ * @throws NotConnectedException if the XMPP connection is not connected.
+ * @throws InterruptedException if the calling thread was interrupted.
*/
- protected List getActions() {
- return data.getActions();
- }
+ private synchronized AdHocCommandResult executeAction(AdHocCommandData.Action action, DataForm form) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
+ AdHocCommandData request = AdHocCommandData.builder(getNode(), connection)
+ .ofType(IQ.Type.set)
+ .to(getOwnerJID())
+ .setSessionId(getSessionId())
+ .setAction(action)
+ .setForm(form)
+ .build();
- /**
- * Add an action to the current stage available actions. This should be used
- * when creating a response.
- *
- * @param action the action.
- */
- protected void addActionAvailable(Action action) {
- data.addAction(action);
- }
+ addRequest(request);
- /**
- * Returns the action available for the current stage which is
- * considered the equivalent to "execute". When the requester sends his
- * reply, if no action was defined in the command then the action will be
- * assumed "execute" thus assuming the action returned by this method. This
- * method will never be invoked for commands that have no stages.
- *
- * @return the action available for the current stage which is considered
- * the equivalent to "execute".
- */
- protected Action getExecuteAction() {
- return data.getExecuteAction();
- }
+ AdHocCommandData response = connection.sendIqRequestAndWaitForResponse(request);
- /**
- * Sets which of the actions available for the current stage is
- * considered the equivalent to "execute". This should be used when setting
- * a response. When the requester sends his reply, if no action was defined
- * in the command then the action will be assumed "execute" thus assuming
- * the action returned by this method.
- *
- * @param action the action.
- */
- protected void setExecuteAction(Action action) {
- data.setExecuteAction(action);
- }
-
- /**
- * Returns the status of the current stage.
- *
- * @return the current status.
- */
- public Status getStatus() {
- return data.getStatus();
- }
-
- /**
- * Check if this command has been completed successfully.
- *
- * @return true
if this command is completed.
- * @since 4.2
- */
- public boolean isCompleted() {
- return getStatus() == Status.completed;
- }
-
- /**
- * Sets the data of the current stage. This should not used.
- *
- * @param data the data.
- */
- void setData(AdHocCommandData data) {
- this.data = data;
- }
-
- /**
- * Gets the data of the current stage. This should not used.
- *
- * @return the data.
- */
- AdHocCommandData getData() {
- return data;
- }
-
- /**
- * Returns true if the action
is available in the current stage.
- * The {@link Action#cancel cancel} action is always allowed. To define the
- * available actions use the addActionAvailable
method.
- *
- * @param action TODO javadoc me please
- * The action to check if it is available.
- * @return True if the action is available for the current stage.
- */
- protected boolean isValidAction(Action action) {
- return getActions().contains(action) || Action.cancel.equals(action);
- }
-
- /**
- * The status of the stage in the adhoc command.
- */
- public enum Status {
-
- /**
- * The command is being executed.
- */
- executing,
-
- /**
- * The command has completed. The command session has ended.
- */
- completed,
-
- /**
- * The command has been canceled. The command session has ended.
- */
- canceled
- }
-
- public enum Action {
-
- /**
- * The command should be executed or continue to be executed. This is
- * the default value.
- */
- execute,
-
- /**
- * The command should be canceled.
- */
- cancel,
-
- /**
- * The command should be digress to the previous stage of execution.
- */
- prev,
-
- /**
- * The command should progress to the next stage of execution.
- */
- next,
-
- /**
- * The command should be completed (if possible).
- */
- complete,
-
- /**
- * The action is unknown. This is used when a received message has an
- * unknown action. It must not be used to send an execution request.
- */
- unknown
- }
-
- public enum SpecificErrorCondition {
-
- /**
- * The responding JID cannot accept the specified action.
- */
- badAction("bad-action"),
-
- /**
- * The responding JID does not understand the specified action.
- */
- malformedAction("malformed-action"),
-
- /**
- * The responding JID cannot accept the specified language/locale.
- */
- badLocale("bad-locale"),
-
- /**
- * The responding JID cannot accept the specified payload (e.g. the data
- * form did not provide one or more required fields).
- */
- badPayload("bad-payload"),
-
- /**
- * The responding JID cannot accept the specified sessionid.
- */
- badSessionid("bad-sessionid"),
-
- /**
- * The requesting JID specified a sessionid that is no longer active
- * (either because it was completed, canceled, or timed out).
- */
- sessionExpired("session-expired");
-
- private final String value;
-
- SpecificErrorCondition(String value) {
- this.value = value;
+ // The Ad-Hoc service ("server") may have generated a session id for us.
+ String sessionId = response.getSessionId();
+ if (sessionId != null) {
+ setSessionId(sessionId);
}
- @Override
- public String toString() {
- return value;
- }
+ AdHocCommandResult result = AdHocCommandResult.from(response);
+ addResult(result);
+ return result;
}
+
}
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/AdHocCommandHandler.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/AdHocCommandHandler.java
new file mode 100755
index 000000000..181fa7c3d
--- /dev/null
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/AdHocCommandHandler.java
@@ -0,0 +1,181 @@
+/**
+ *
+ * Copyright 2005-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.commands;
+
+import org.jivesoftware.smack.SmackException.NoResponseException;
+import org.jivesoftware.smack.SmackException.NotConnectedException;
+import org.jivesoftware.smack.XMPPException.XMPPErrorException;
+import org.jivesoftware.smack.packet.StanzaError;
+import org.jivesoftware.smackx.commands.packet.AdHocCommandData;
+import org.jivesoftware.smackx.commands.packet.AdHocCommandDataBuilder;
+import org.jivesoftware.smackx.xdata.form.SubmitForm;
+
+import org.jxmpp.jid.Jid;
+
+/**
+ * Represents a command that can be executed locally from a remote location. This
+ * class must be extended to implement an specific ad-hoc command. This class
+ * provides some useful tools:
+ * - Node
+ * - Name
+ * - Session ID
+ * - Current Stage
+ * - Available actions
+ * - Default action
+ *
+ * To implement a new command extend this class and implement all the abstract
+ * methods. When implementing the actions remember that they could be invoked
+ * several times, and that you must use the current stage number to know what to
+ * do.
+ *
+ * @author Gabriel Guardincerri
+ * @author Florian Schmaus
+ */
+public abstract class AdHocCommandHandler extends AbstractAdHocCommand {
+
+ /**
+ * The time stamp of first invocation of the command. Used to implement the session timeout.
+ */
+ private final long creationDate;
+
+ /**
+ * The number of the current stage.
+ */
+ private int currentStage;
+
+ public AdHocCommandHandler(String node, String name, String sessionId) {
+ super(node, name);
+ setSessionId(sessionId);
+ this.creationDate = System.currentTimeMillis();
+ }
+
+ protected abstract AdHocCommandData execute(AdHocCommandDataBuilder response) throws NoResponseException,
+ XMPPErrorException, NotConnectedException, InterruptedException, IllegalStateException;
+
+ protected abstract AdHocCommandData next(AdHocCommandDataBuilder response, SubmitForm submittedForm)
+ throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException,
+ IllegalStateException;
+
+ protected abstract AdHocCommandData complete(AdHocCommandDataBuilder response, SubmitForm submittedForm)
+ throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException,
+ IllegalStateException;
+
+ protected abstract AdHocCommandData prev(AdHocCommandDataBuilder response) throws NoResponseException,
+ XMPPErrorException, NotConnectedException, InterruptedException, IllegalStateException;
+
+ /**
+ * Returns the date the command was created.
+ *
+ * @return the date the command was created.
+ */
+ public long getCreationDate() {
+ return creationDate;
+ }
+
+ /**
+ * Returns true if the specified requester has permission to execute all the
+ * stages of this action. This is checked when the first request is received,
+ * if the permission is grant then the requester will be able to execute
+ * all the stages of the command. It is not checked again during the
+ * execution.
+ *
+ * @param jid the JID to check permissions on.
+ * @return true if the user has permission to execute this action.
+ */
+ public boolean hasPermission(Jid jid) {
+ return true;
+ };
+
+ /**
+ * Returns the currently executing stage number. The first stage number is
+ * 1. During the execution of the first action this method will answer 1.
+ *
+ * @return the current stage number.
+ */
+ public final int getCurrentStage() {
+ return currentStage;
+ }
+
+ /**
+ * Increase the current stage number.
+ */
+ final void incrementStage() {
+ currentStage++;
+ }
+
+ /**
+ * Decrease the current stage number.
+ */
+ final void decrementStage() {
+ currentStage--;
+ }
+
+ protected static XMPPErrorException newXmppErrorException(StanzaError.Condition condition) {
+ return newXmppErrorException(condition, null);
+ }
+
+ protected static XMPPErrorException newXmppErrorException(StanzaError.Condition condition, String descriptiveText) {
+ StanzaError stanzaError = StanzaError.from(condition, descriptiveText).build();
+ return new XMPPErrorException(null, stanzaError);
+ }
+
+ protected static XMPPErrorException newBadRequestException(String descriptiveTest) {
+ return newXmppErrorException(StanzaError.Condition.bad_request, descriptiveTest);
+ }
+
+ public abstract static class SingleStage extends AdHocCommandHandler {
+
+ public SingleStage(String node, String name, String sessionId) {
+ super(node, name, sessionId);
+ }
+
+ protected abstract AdHocCommandData executeSingleStage(AdHocCommandDataBuilder response)
+ throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException;
+
+ @Override
+ protected final AdHocCommandData execute(AdHocCommandDataBuilder response)
+ throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
+ response.setStatusCompleted();
+ return executeSingleStage(response);
+ }
+
+ @Override
+ public final AdHocCommandData next(AdHocCommandDataBuilder response, SubmitForm submittedForm)
+ throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
+ throw newXmppErrorException(StanzaError.Condition.bad_request);
+ }
+
+ @Override
+ public final AdHocCommandData complete(AdHocCommandDataBuilder response, SubmitForm submittedForm)
+ throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
+ throw newXmppErrorException(StanzaError.Condition.bad_request);
+ }
+
+ @Override
+ public final AdHocCommandData prev(AdHocCommandDataBuilder response)
+ throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
+ throw newXmppErrorException(StanzaError.Condition.bad_request);
+ }
+
+ @Override
+ public final void cancel()
+ throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
+ throw newXmppErrorException(StanzaError.Condition.bad_request);
+ }
+
+ }
+}
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/LocalCommandFactory.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/AdHocCommandHandlerFactory.java
similarity index 64%
rename from smack-extensions/src/main/java/org/jivesoftware/smackx/commands/LocalCommandFactory.java
rename to smack-extensions/src/main/java/org/jivesoftware/smackx/commands/AdHocCommandHandlerFactory.java
index bc635d72a..1ea08477b 100644
--- a/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/LocalCommandFactory.java
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/AdHocCommandHandlerFactory.java
@@ -19,28 +19,31 @@ package org.jivesoftware.smackx.commands;
import java.lang.reflect.InvocationTargetException;
/**
- * A factory for creating local commands. It's useful in cases where instantiation
+ * A factory for creating ad-hoc command handlers. It's useful in cases where instantiation
* of a command is more complicated than just using the default constructor. For example,
* when arguments must be passed into the constructor or when using a dependency injection
- * framework. When a LocalCommandFactory isn't used, you can provide the AdHocCommandManager
+ * framework. When a factory isn't used, you can provide the AdHocCommandManager
* a Class object instead. For more details, see
- * {@link AdHocCommandManager#registerCommand(String, String, LocalCommandFactory)}.
+ * {@link AdHocCommandManager#registerCommand(String, String, AdHocCommandHandlerFactory)}.
*
* @author Matt Tucker
*/
-public interface LocalCommandFactory {
+public interface AdHocCommandHandlerFactory {
/**
- * Returns an instance of a LocalCommand.
+ * Returns a new instance of an ad-hoc command handler.
*
+ * @param node the node of the ad-hoc command.
+ * @param name the name of the ad-hoc command.
+ * @param sessionId the session ID of the ad-hoc command.
* @return a LocalCommand instance.
* @throws InstantiationException if creating an instance failed.
* @throws IllegalAccessException if creating an instance is not allowed.
- * @throws SecurityException if there was a security violation.
- * @throws NoSuchMethodException if no such method is declared
* @throws InvocationTargetException if a reflection-based method or constructor invocation threw.
* @throws IllegalArgumentException if an illegal argument was given.
*/
- LocalCommand getInstance() throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException;
+ AdHocCommandHandler create(String node, String name, String sessionId)
+ throws InstantiationException, IllegalAccessException, IllegalArgumentException,
+ InvocationTargetException;
}
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 aaad5c0ee..49bf9c0cd 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
@@ -17,6 +17,7 @@
package org.jivesoftware.smackx.commands;
+import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
@@ -44,16 +45,17 @@ import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.StanzaError;
import org.jivesoftware.smack.util.StringUtils;
-import org.jivesoftware.smackx.commands.AdHocCommand.Action;
-import org.jivesoftware.smackx.commands.AdHocCommand.Status;
import org.jivesoftware.smackx.commands.packet.AdHocCommandData;
+import org.jivesoftware.smackx.commands.packet.AdHocCommandData.AllowedAction;
+import org.jivesoftware.smackx.commands.packet.AdHocCommandDataBuilder;
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.FillableForm;
+import org.jivesoftware.smackx.xdata.form.SubmitForm;
import org.jivesoftware.smackx.xdata.packet.DataForm;
+import org.jxmpp.jid.EntityFullJid;
import org.jxmpp.jid.Jid;
/**
@@ -65,6 +67,7 @@ import org.jxmpp.jid.Jid;
* get an instance of this class.
*
* @author Gabriel Guardincerri
+ * @author Florian Schmaus
*/
public final class AdHocCommandManager extends Manager {
public static final String NAMESPACE = "http://jabber.org/protocol/commands";
@@ -74,7 +77,7 @@ public final class AdHocCommandManager extends Manager {
/**
* The session time out in seconds.
*/
- private static final int SESSION_TIMEOUT = 2 * 60;
+ private static int DEFAULT_SESSION_TIMEOUT_SECS = 7 * 60;
/**
* Map an XMPPConnection with it AdHocCommandManager. This map have a key-value
@@ -91,7 +94,7 @@ public final class AdHocCommandManager extends Manager {
XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() {
@Override
public void connectionCreated(XMPPConnection connection) {
- getAddHocCommandsManager(connection);
+ getInstance(connection);
}
});
}
@@ -102,8 +105,21 @@ public final class AdHocCommandManager extends Manager {
*
* @param connection the XMPP connection.
* @return the AdHocCommandManager associated with the connection.
+ * @deprecated use {@link #getInstance(XMPPConnection)} instead.
*/
- public static synchronized AdHocCommandManager getAddHocCommandsManager(XMPPConnection connection) {
+ @Deprecated
+ public static AdHocCommandManager getAddHocCommandsManager(XMPPConnection connection) {
+ return getInstance(connection);
+ }
+
+ /**
+ * Returns the AdHocCommandManager
related to the
+ * connection
.
+ *
+ * @param connection the XMPP connection.
+ * @return the AdHocCommandManager associated with the connection.
+ */
+ public static synchronized AdHocCommandManager getInstance(XMPPConnection connection) {
AdHocCommandManager ahcm = instances.get(connection);
if (ahcm == null) {
ahcm = new AdHocCommandManager(connection);
@@ -117,7 +133,8 @@ public final class AdHocCommandManager extends Manager {
* Value=command. Command node matches the node attribute sent by command
* requesters.
*/
- private final Map commands = new ConcurrentHashMap<>();
+ // TODO: Change to Map once Smack's minimum Android API level is 24 or higher.
+ private final ConcurrentHashMap commands = new ConcurrentHashMap<>();
/**
* Map a command session ID with the instance LocalCommand. The LocalCommand
@@ -125,10 +142,12 @@ public final class AdHocCommandManager extends Manager {
* the command execution. Note: Key=session ID, Value=LocalCommand. Session
* ID matches the sessionid attribute sent by command responders.
*/
- private final Map executingCommands = new ConcurrentHashMap<>();
+ private final Map executingCommands = new ConcurrentHashMap<>();
private final ServiceDiscoveryManager serviceDiscoveryManager;
+ private int sessionTimeoutSecs = DEFAULT_SESSION_TIMEOUT_SECS;
+
private AdHocCommandManager(XMPPConnection connection) {
super(connection);
this.serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection);
@@ -148,13 +167,17 @@ public final class AdHocCommandManager extends Manager {
new AbstractNodeInformationProvider() {
@Override
public List getNodeItems() {
-
List answer = new ArrayList<>();
- Collection commandsList = getRegisteredCommands();
+ Collection commandsList = commands.values();
+
+ EntityFullJid ourJid = connection().getUser();
+ if (ourJid == null) {
+ LOGGER.warning("Local connection JID not available, can not respond to " + NAMESPACE + " node information");
+ return null;
+ }
for (AdHocCommandInfo info : commandsList) {
- DiscoverItems.Item item = new DiscoverItems.Item(
- info.getOwnerJID());
+ DiscoverItems.Item item = new DiscoverItems.Item(ourJid);
item.setName(info.getName());
item.setNode(info.getNode());
answer.add(item);
@@ -166,18 +189,17 @@ public final class AdHocCommandManager extends Manager {
// The packet listener and the filter for processing some AdHoc Commands
// Packets
+ // TODO: This handler being async means that requests for the same command could be handled out of order. Nobody
+ // complained so far, and I could imagine that it does not really matter in practice. But it is certainly
+ // something to keep in mind.
connection.registerIQRequestHandler(new AbstractIqRequestHandler(AdHocCommandData.ELEMENT,
AdHocCommandData.NAMESPACE, IQ.Type.set, Mode.async) {
@Override
public IQ handleIQRequest(IQ iqRequest) {
AdHocCommandData requestData = (AdHocCommandData) iqRequest;
- try {
- return processAdHocCommand(requestData);
- }
- catch (InterruptedException | NoResponseException | NotConnectedException e) {
- LOGGER.log(Level.INFO, "processAdHocCommand threw exception", e);
- return null;
- }
+ AdHocCommandData response = processAdHocCommand(requestData);
+ assert response.getStatus() != null || response.getType() == IQ.Type.error;
+ return response;
}
});
}
@@ -187,18 +209,21 @@ public final class AdHocCommandManager extends Manager {
* connection. The node
is an unique identifier of that command for
* the connection related to this command manager. The name
is the
* human readable name of the command. The class
is the class of
- * the command, which must extend {@link LocalCommand} and have a default
+ * the command, which must extend {@link AdHocCommandHandler} and have a default
* constructor.
*
* @param node the unique identifier of the command.
* @param name the human readable name of the command.
- * @param clazz the class of the command, which must extend {@link LocalCommand}.
+ * @param clazz the class of the command, which must extend {@link AdHocCommandHandler}.
+ * @throws SecurityException if there was a security violation.
+ * @throws NoSuchMethodException if no such method is declared.
*/
- public void registerCommand(String node, String name, final Class extends LocalCommand> clazz) {
- registerCommand(node, name, new LocalCommandFactory() {
+ public void registerCommand(String node, String name, final Class extends AdHocCommandHandler> clazz) throws NoSuchMethodException, SecurityException {
+ Constructor extends AdHocCommandHandler> constructor = clazz.getConstructor(String.class, String.class, String.class);
+ registerCommand(node, name, new AdHocCommandHandlerFactory() {
@Override
- public LocalCommand getInstance() throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException {
- return clazz.getConstructor().newInstance();
+ public AdHocCommandHandler create(String node, String name, String sessionId) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
+ return constructor.newInstance(node, name, sessionId);
}
});
}
@@ -214,10 +239,12 @@ public final class AdHocCommandManager extends Manager {
* @param name the human readable name of the command.
* @param factory a factory to create new instances of the command.
*/
- public void registerCommand(String node, final String name, LocalCommandFactory factory) {
- AdHocCommandInfo commandInfo = new AdHocCommandInfo(node, name, connection().getUser(), factory);
+ public synchronized void registerCommand(String node, final String name, AdHocCommandHandlerFactory factory) {
+ AdHocCommandInfo commandInfo = new AdHocCommandInfo(node, name, factory);
+
+ AdHocCommandInfo existing = commands.putIfAbsent(node, commandInfo);
+ if (existing != null) throw new IllegalArgumentException("There is already an ad-hoc command registered for " + node);
- commands.put(node, commandInfo);
// Set the NodeInformationProvider that will provide information about
// the added command
serviceDiscoveryManager.setNodeInformationProvider(node,
@@ -242,6 +269,14 @@ public final class AdHocCommandManager extends Manager {
});
}
+ public synchronized boolean unregisterCommand(String node) {
+ AdHocCommandInfo commandInfo = commands.remove(node);
+ if (commandInfo == null) return false;
+
+ serviceDiscoveryManager.removeNodeInformationProvider(node);
+ return true;
+ }
+
/**
* Discover the commands of an specific JID. The jid
is a
* full JID.
@@ -266,8 +301,8 @@ public final class AdHocCommandManager extends Manager {
* @param node the identifier of the command
* @return a local instance equivalent to the remote command.
*/
- public RemoteCommand getRemoteCommand(Jid jid, String node) {
- return new RemoteCommand(connection(), node, jid);
+ public AdHocCommand getRemoteCommand(Jid jid, String node) {
+ return new AdHocCommand(connection(), node, jid);
}
/**
@@ -291,240 +326,226 @@ public final class AdHocCommandManager extends Manager {
* The action to execute is one of the available actions
*
*
- * @param requestData TODO javadoc me please
- * the stanza to process.
- * @throws NotConnectedException if the XMPP connection is not connected.
- * @throws NoResponseException if there was no response from the remote entity.
- * @throws InterruptedException if the calling thread was interrupted.
+ * @param request the incoming AdHoc command request.
*/
- private IQ processAdHocCommand(AdHocCommandData requestData) throws NoResponseException, NotConnectedException, InterruptedException {
- // Creates the response with the corresponding data
- AdHocCommandData response = new AdHocCommandData();
- response.setTo(requestData.getFrom());
- response.setStanzaId(requestData.getStanzaId());
- response.setNode(requestData.getNode());
- response.setId(requestData.getTo());
-
- String sessionId = requestData.getSessionID();
- String commandNode = requestData.getNode();
+ private AdHocCommandData processAdHocCommand(AdHocCommandData request) {
+ String sessionId = request.getSessionId();
+ final AdHocCommandHandler command;
if (sessionId == null) {
+ String commandNode = request.getNode();
+
// A new execution request has been received. Check that the
// command exists
- if (!commands.containsKey(commandNode)) {
+ AdHocCommandInfo commandInfo = commands.get(commandNode);
+ if (commandInfo == null) {
// Requested command does not exist so return
// item_not_found error.
- return respondError(response, StanzaError.Condition.item_not_found);
+ return respondError(request, null, StanzaError.Condition.item_not_found);
}
- // Create new session ID
- sessionId = StringUtils.randomString(15);
+ assert commandInfo.getNode().equals(commandNode);
+ // Create a new instance of the command with the
+ // corresponding session ID.
try {
- // Create a new instance of the command with the
- // corresponding sessionid
- LocalCommand command;
- try {
- command = newInstanceOfCmd(commandNode, sessionId);
- }
- catch (InstantiationException | IllegalAccessException | IllegalArgumentException
- | InvocationTargetException | NoSuchMethodException | SecurityException e) {
- StanzaError xmppError = StanzaError.getBuilder()
- .setCondition(StanzaError.Condition.internal_server_error)
- .setDescriptiveEnText(e.getMessage())
- .build();
- return respondError(response, xmppError);
- }
-
- response.setType(IQ.Type.result);
- command.setData(response);
-
- // Check that the requester has enough permission.
- // Answer forbidden error if requester permissions are not
- // enough to execute the requested command
- if (!command.hasPermission(requestData.getFrom())) {
- return respondError(response, StanzaError.Condition.forbidden);
- }
-
- Action action = requestData.getAction();
-
- // If the action is unknown then respond an error.
- if (action != null && action.equals(Action.unknown)) {
- return respondError(response, StanzaError.Condition.bad_request,
- AdHocCommand.SpecificErrorCondition.malformedAction);
- }
-
- // If the action is not execute, then it is an invalid action.
- if (action != null && !action.equals(Action.execute)) {
- return respondError(response, StanzaError.Condition.bad_request,
- AdHocCommand.SpecificErrorCondition.badAction);
- }
-
- // Increase the state number, so the command knows in witch
- // stage it is
- command.incrementStage();
- // Executes the command
- command.execute();
-
- if (command.isLastStage()) {
- // If there is only one stage then the command is completed
- response.setStatus(Status.completed);
- }
- else {
- // Else it is still executing, and is registered to be
- // available for the next call
- response.setStatus(Status.executing);
- executingCommands.put(sessionId, command);
- // See if the session sweeper thread is scheduled. If not, start it.
- maybeWindUpSessionSweeper();
- }
-
- // Sends the response packet
- return response;
-
+ command = commandInfo.getCommandInstance();
}
- catch (XMPPErrorException e) {
- // If there is an exception caused by the next, complete,
- // prev or cancel method, then that error is returned to the
- // requester.
- StanzaError error = e.getStanzaError();
-
- // If the error type is cancel, then the execution is
- // canceled therefore the status must show that, and the
- // command be removed from the executing list.
- if (StanzaError.Type.CANCEL.equals(error.getType())) {
- response.setStatus(Status.canceled);
- executingCommands.remove(sessionId);
- }
- return respondError(response, error);
+ catch (InstantiationException | IllegalAccessException | IllegalArgumentException
+ | InvocationTargetException e) {
+ LOGGER.log(Level.WARNING, "Could not instanciate ad-hoc command server", e);
+ StanzaError xmppError = StanzaError.getBuilder()
+ .setCondition(StanzaError.Condition.internal_server_error)
+ .setDescriptiveEnText(e.getMessage())
+ .build();
+ return respondError(request, null, xmppError);
}
- }
- else {
- LocalCommand command = executingCommands.get(sessionId);
-
+ } else {
+ command = executingCommands.get(sessionId);
// Check that a command exists for the specified sessionID
// This also handles if the command was removed in the meanwhile
// of getting the key and the value of the map.
if (command == null) {
- return respondError(response, StanzaError.Condition.bad_request,
- AdHocCommand.SpecificErrorCondition.badSessionid);
- }
-
- // Check if the Session data has expired (default is 10 minutes)
- long creationStamp = command.getCreationDate();
- if (System.currentTimeMillis() - creationStamp > SESSION_TIMEOUT * 1000) {
- // Remove the expired session
- executingCommands.remove(sessionId);
-
- // Answer a not_allowed error (session-expired)
- return respondError(response, StanzaError.Condition.not_allowed,
- AdHocCommand.SpecificErrorCondition.sessionExpired);
- }
-
- /*
- * Since the requester could send two requests for the same
- * executing command i.e. the same session id, all the execution of
- * the action must be synchronized to avoid inconsistencies.
- */
- synchronized (command) {
- Action action = requestData.getAction();
-
- // If the action is unknown the respond an error
- if (action != null && action.equals(Action.unknown)) {
- return respondError(response, StanzaError.Condition.bad_request,
- AdHocCommand.SpecificErrorCondition.malformedAction);
- }
-
- // If the user didn't specify an action or specify the execute
- // action then follow the actual default execute action
- if (action == null || Action.execute.equals(action)) {
- action = command.getExecuteAction();
- }
-
- // Check that the specified action was previously
- // offered
- if (!command.isValidAction(action)) {
- return respondError(response, StanzaError.Condition.bad_request,
- AdHocCommand.SpecificErrorCondition.badAction);
- }
-
- try {
- // TODO: Check that all the required fields of the form are
- // TODO: filled, if not throw an exception. This will simplify the
- // TODO: construction of new commands
-
- // Since all errors were passed, the response is now a
- // result
- response.setType(IQ.Type.result);
-
- // Set the new data to the command.
- command.setData(response);
-
- if (Action.next.equals(action)) {
- command.incrementStage();
- DataForm dataForm = requestData.getForm();
- command.next(new FillableForm(dataForm));
- if (command.isLastStage()) {
- // If it is the last stage then the command is
- // completed
- response.setStatus(Status.completed);
- }
- else {
- // Otherwise it is still executing
- response.setStatus(Status.executing);
- }
- }
- else if (Action.complete.equals(action)) {
- command.incrementStage();
- DataForm dataForm = requestData.getForm();
- command.complete(new FillableForm(dataForm));
- response.setStatus(Status.completed);
- // Remove the completed session
- executingCommands.remove(sessionId);
- }
- else if (Action.prev.equals(action)) {
- command.decrementStage();
- command.prev();
- }
- else if (Action.cancel.equals(action)) {
- command.cancel();
- response.setStatus(Status.canceled);
- // Remove the canceled session
- executingCommands.remove(sessionId);
- }
-
- return response;
- }
- catch (XMPPErrorException e) {
- // If there is an exception caused by the next, complete,
- // prev or cancel method, then that error is returned to the
- // requester.
- StanzaError error = e.getStanzaError();
-
- // If the error type is cancel, then the execution is
- // canceled therefore the status must show that, and the
- // command be removed from the executing list.
- if (StanzaError.Type.CANCEL.equals(error.getType())) {
- response.setStatus(Status.canceled);
- executingCommands.remove(sessionId);
- }
- return respondError(response, error);
- }
+ return respondError(request, null, StanzaError.Condition.bad_request,
+ SpecificErrorCondition.badSessionid);
}
}
+
+
+ final AdHocCommandDataBuilder responseBuilder = AdHocCommandDataBuilder.buildResponseFor(request)
+ .setSessionId(command.getSessionId());
+
+ final AdHocCommandData response;
+ /*
+ * Since the requester could send two requests for the same
+ * executing command i.e. the same session id, all the execution of
+ * the action must be synchronized to avoid inconsistencies.
+ */
+ synchronized (command) {
+ command.addRequest(request);
+
+ if (sessionId == null) {
+ response = processAdHocCommandOfNewSession(request, command, responseBuilder);
+ } else {
+ response = processAdHocCommandOfExistingSession(request, command, responseBuilder);
+ }
+
+
+ AdHocCommandResult commandResult = AdHocCommandResult.from(response);
+ command.addResult(commandResult);
+ }
+
+ return response;
+ }
+
+ private AdHocCommandData createResponseFrom(AdHocCommandData request, AdHocCommandDataBuilder response, XMPPErrorException exception, String sessionId) {
+ StanzaError error = exception.getStanzaError();
+
+ // If the error type is cancel, then the execution is
+ // canceled therefore the status must show that, and the
+ // command be removed from the executing list.
+ if (error.getType() == StanzaError.Type.CANCEL) {
+ response.setStatus(AdHocCommandData.Status.canceled);
+
+ executingCommands.remove(sessionId);
+
+ return response.build();
+ }
+
+ return respondError(request, response, error);
+ }
+
+ private static AdHocCommandData createResponseFrom(AdHocCommandData request, AdHocCommandDataBuilder response, Exception exception) {
+ StanzaError error = StanzaError.from(StanzaError.Condition.internal_server_error, exception.getMessage())
+ .build();
+ return respondError(request, response, error);
+ }
+
+ private AdHocCommandData processAdHocCommandOfNewSession(AdHocCommandData request, AdHocCommandHandler command, AdHocCommandDataBuilder responseBuilder) {
+ // Check that the requester has enough permission.
+ // Answer forbidden error if requester permissions are not
+ // enough to execute the requested command
+ if (!command.hasPermission(request.getFrom())) {
+ return respondError(request, responseBuilder, StanzaError.Condition.forbidden);
+ }
+
+ AdHocCommandData.Action action = request.getAction();
+
+ // If the action is not execute, then it is an invalid action.
+ if (action != null && !action.equals(AdHocCommandData.Action.execute)) {
+ return respondError(request, responseBuilder, StanzaError.Condition.bad_request,
+ SpecificErrorCondition.badAction);
+ }
+
+ // Increase the state number, so the command knows in witch
+ // stage it is
+ command.incrementStage();
+
+ final AdHocCommandData response;
+ try {
+ // Executes the command
+ response = command.execute(responseBuilder);
+ } catch (XMPPErrorException e) {
+ return createResponseFrom(request, responseBuilder, e, command.getSessionId());
+ } catch (NoResponseException | NotConnectedException | InterruptedException | IllegalStateException e) {
+ return createResponseFrom(request, responseBuilder, e);
+ }
+
+ if (response.isExecuting()) {
+ executingCommands.put(command.getSessionId(), command);
+ // See if the session sweeper thread is scheduled. If not, start it.
+ maybeWindUpSessionSweeper();
+ }
+
+ return response;
+ }
+
+ private AdHocCommandData processAdHocCommandOfExistingSession(AdHocCommandData request, AdHocCommandHandler command, AdHocCommandDataBuilder responseBuilder) {
+ // Check if the Session data has expired (default is 10 minutes)
+ long creationStamp = command.getCreationDate();
+ if (System.currentTimeMillis() - creationStamp > sessionTimeoutSecs * 1000) {
+ // Remove the expired session
+ executingCommands.remove(command.getSessionId());
+
+ // Answer a not_allowed error (session-expired)
+ return respondError(request, responseBuilder, StanzaError.Condition.not_allowed,
+ SpecificErrorCondition.sessionExpired);
+ }
+
+ AdHocCommandData.Action action = request.getAction();
+
+ // If the user didn't specify an action or specify the execute
+ // action then follow the actual default execute action
+ if (action == null || AdHocCommandData.Action.execute.equals(action)) {
+ AllowedAction executeAction = command.getExecuteAction();
+ if (executeAction != null) {
+ action = executeAction.action;
+ }
+ }
+
+ // Check that the specified action was previously
+ // offered
+ if (!command.isValidAction(action)) {
+ return respondError(request, responseBuilder, StanzaError.Condition.bad_request,
+ SpecificErrorCondition.badAction);
+ }
+
+ AdHocCommandData response;
+ try {
+ DataForm dataForm;
+ switch (action) {
+ case next:
+ command.incrementStage();
+ dataForm = request.getForm();
+ response = command.next(responseBuilder, new SubmitForm(dataForm));
+ break;
+ case complete:
+ command.incrementStage();
+ dataForm = request.getForm();
+ responseBuilder.setStatus(AdHocCommandData.Status.completed);
+ response = command.complete(responseBuilder, new SubmitForm(dataForm));
+ // Remove the completed session
+ executingCommands.remove(command.getSessionId());
+ break;
+ case prev:
+ command.decrementStage();
+ response = command.prev(responseBuilder);
+ break;
+ case cancel:
+ command.cancel();
+ responseBuilder.setStatus(AdHocCommandData.Status.canceled);
+ response = responseBuilder.build();
+ // Remove the canceled session
+ executingCommands.remove(command.getSessionId());
+ break;
+ default:
+ return respondError(request, responseBuilder, StanzaError.Condition.bad_request,
+ SpecificErrorCondition.badAction);
+ }
+ } catch (XMPPErrorException e) {
+ return createResponseFrom(request, responseBuilder, e, command.getSessionId());
+ } catch (NoResponseException | NotConnectedException | InterruptedException | IllegalStateException e) {
+ return createResponseFrom(request, responseBuilder, e);
+ }
+
+ return response;
}
private boolean sessionSweeperScheduled;
+ private int getSessionRemovalTimeoutSecs() {
+ return sessionTimeoutSecs * 2;
+ }
+
private void sessionSweeper() {
final long currentTime = System.currentTimeMillis();
synchronized (this) {
- for (Iterator> it = executingCommands.entrySet().iterator(); it.hasNext();) {
- Map.Entry entry = it.next();
- LocalCommand command = entry.getValue();
+ for (Iterator> it = executingCommands.entrySet().iterator(); it.hasNext();) {
+ Map.Entry entry = it.next();
+ AdHocCommandHandler command = entry.getValue();
long creationStamp = command.getCreationDate();
- // Check if the Session data has expired (default is 10 minutes)
+ // Check if the Session data has expired.
// To remove it from the session list it waits for the double of
// the of time out time. This is to let
// the requester know why his execution request is
@@ -532,7 +553,7 @@ public final class AdHocCommandManager extends Manager {
// after the time out, then once the user requests to
// continue the execution he will received an
// invalid session error and not a time out error.
- if (currentTime - creationStamp > SESSION_TIMEOUT * 1000 * 2) {
+ if (currentTime - creationStamp > getSessionRemovalTimeoutSecs() * 1000) {
// Remove the expired session
it.remove();
}
@@ -552,104 +573,100 @@ public final class AdHocCommandManager extends Manager {
}
sessionSweeperScheduled = true;
- schedule(this::sessionSweeper, 10, TimeUnit.SECONDS);
+ schedule(this::sessionSweeper, getSessionRemovalTimeoutSecs() + 1, TimeUnit.SECONDS);
}
/**
* Responds an error with an specific condition.
*
- * @param response the response to send.
+ * @param request the request that caused the error response.
* @param condition the condition of the error.
*/
- private static IQ respondError(AdHocCommandData response,
+ private static AdHocCommandData respondError(AdHocCommandData request, AdHocCommandDataBuilder response,
StanzaError.Condition condition) {
- return respondError(response, StanzaError.getBuilder(condition).build());
+ return respondError(request, response, StanzaError.getBuilder(condition).build());
}
/**
* Responds an error with an specific condition.
*
- * @param response the response to send.
+ * @param request the request that caused the error response.
* @param condition the condition of the error.
* @param specificCondition the adhoc command error condition.
*/
- private static IQ respondError(AdHocCommandData response, StanzaError.Condition condition,
- AdHocCommand.SpecificErrorCondition specificCondition) {
+ private static AdHocCommandData respondError(AdHocCommandData request, AdHocCommandDataBuilder response, StanzaError.Condition condition,
+ SpecificErrorCondition specificCondition) {
StanzaError error = StanzaError.getBuilder(condition)
.addExtension(new AdHocCommandData.SpecificError(specificCondition))
.build();
- return respondError(response, error);
+ return respondError(request, response, error);
}
/**
* Responds an error with an specific error.
*
- * @param response the response to send.
+ * @param request the request that caused the error response.
* @param error the error to send.
*/
- private static IQ respondError(AdHocCommandData response, StanzaError error) {
- response.setType(IQ.Type.error);
- response.setError(error);
- return response;
+ private static AdHocCommandData respondError(AdHocCommandData request, AdHocCommandDataBuilder response, StanzaError error) {
+ if (response == null) {
+ return AdHocCommandDataBuilder.buildResponseFor(request, IQ.ResponseType.error).setError(error).build();
+ }
+
+ // Response may be not of IQ type error here, so switch that.
+ return response.ofType(IQ.Type.error)
+ .setError(error)
+ .build();
}
- /**
- * Creates a new instance of a command to be used by a new execution request
- *
- * @param commandNode the command node that identifies it.
- * @param sessionID the session id of this execution.
- * @return the command instance to execute.
- * @throws XMPPErrorException if there is problem creating the new instance.
- * @throws SecurityException if there was a security violation.
- * @throws NoSuchMethodException if no such method is declared
- * @throws InvocationTargetException if a reflection-based method or constructor invocation threw.
- * @throws IllegalArgumentException if an illegal argument was given.
- * @throws IllegalAccessException in case of an illegal access.
- * @throws InstantiationException in case of an instantiation error.
- */
- private LocalCommand newInstanceOfCmd(String commandNode, String sessionID)
- throws XMPPErrorException, InstantiationException, IllegalAccessException, IllegalArgumentException,
- InvocationTargetException, NoSuchMethodException, SecurityException {
- AdHocCommandInfo commandInfo = commands.get(commandNode);
- LocalCommand command = commandInfo.getCommandInstance();
- command.setSessionID(sessionID);
- command.setName(commandInfo.getName());
- command.setNode(commandInfo.getNode());
-
- return command;
+ public static void setDefaultSessionTimeoutSecs(int seconds) {
+ if (seconds < 10) {
+ throw new IllegalArgumentException();
+ }
+ DEFAULT_SESSION_TIMEOUT_SECS = seconds;
}
- /**
- * Returns the registered commands of this command manager, which is related
- * to a connection.
- *
- * @return the registered commands.
- */
- private Collection getRegisteredCommands() {
- return commands.values();
+ public void setSessionTimeoutSecs(int seconds) {
+ if (seconds < 10) {
+ throw new IllegalArgumentException();
+ }
+
+ sessionTimeoutSecs = seconds;
}
/**
* Stores ad-hoc command information.
*/
- private static final class AdHocCommandInfo {
+ private final class AdHocCommandInfo {
- private String node;
- private String name;
- private final Jid ownerJID;
- private LocalCommandFactory factory;
+ private final String node;
+ private final String name;
+ private final AdHocCommandHandlerFactory factory;
- private AdHocCommandInfo(String node, String name, Jid ownerJID,
- LocalCommandFactory factory) {
+ private static final int MAX_SESSION_GEN_ATTEMPTS = 3;
+
+ private AdHocCommandInfo(String node, String name, AdHocCommandHandlerFactory factory) {
this.node = node;
this.name = name;
- this.ownerJID = ownerJID;
this.factory = factory;
}
- public LocalCommand getCommandInstance() throws InstantiationException,
- IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException {
- return factory.getInstance();
+ public AdHocCommandHandler getCommandInstance() throws InstantiationException,
+ IllegalAccessException, IllegalArgumentException, InvocationTargetException {
+ String sessionId;
+ // TODO: The code below contains a race condition. Use CopncurrentHashMap.computeIfAbsent() to remove the
+ // race condition once Smack's minimum Android API level 24 or higher.
+ int attempt = 0;
+ do {
+ attempt++;
+ if (attempt > MAX_SESSION_GEN_ATTEMPTS) {
+ throw new RuntimeException("Failed to compute unique session ID");
+ }
+ // Create new session ID
+ sessionId = StringUtils.randomString(15);
+ } while (executingCommands.containsKey(sessionId));
+
+ return factory.create(node, name, sessionId);
}
public String getName() {
@@ -660,8 +677,5 @@ public final class AdHocCommandManager extends Manager {
return node;
}
- public Jid getOwnerJID() {
- return ownerJID;
- }
}
}
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/AdHocCommandResult.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/AdHocCommandResult.java
new file mode 100644
index 000000000..e0927e277
--- /dev/null
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/AdHocCommandResult.java
@@ -0,0 +1,102 @@
+/**
+ *
+ * Copyright 2023 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.commands;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smackx.commands.packet.AdHocCommandData;
+import org.jivesoftware.smackx.xdata.form.FillableForm;
+import org.jivesoftware.smackx.xdata.packet.DataForm;
+
+// TODO: Make this a sealed class once Smack is Java 17 or higher.
+public abstract class AdHocCommandResult {
+
+ private final AdHocCommandData response;
+ private final boolean completed;
+
+ private AdHocCommandResult(AdHocCommandData response, boolean completed) {
+ this.response = response;
+ this.completed = completed;
+ }
+
+ public final AdHocCommandData getResponse() {
+ return response;
+ }
+
+ public final boolean isCompleted() {
+ return completed;
+ }
+
+ public StatusExecuting asExecutingOrThrow() {
+ if (this instanceof StatusExecuting)
+ return (StatusExecuting) this;
+
+ throw new IllegalStateException();
+ }
+
+ public StatusCompleted asCompletedOrThrow() {
+ if (this instanceof StatusCompleted)
+ return (StatusCompleted) this;
+
+ throw new IllegalStateException();
+ }
+
+ public static final class StatusExecuting extends AdHocCommandResult {
+ private StatusExecuting(AdHocCommandData response) {
+ super(response, false);
+ assert response.getStatus() == AdHocCommandData.Status.executing;
+ }
+
+ public FillableForm getFillableForm() {
+ DataForm form = getResponse().getForm();
+ return new FillableForm(form);
+ }
+ }
+
+ public static final class StatusCompleted extends AdHocCommandResult {
+ private StatusCompleted(AdHocCommandData response) {
+ super(response, true);
+ assert response.getStatus() == AdHocCommandData.Status.completed;
+ }
+ }
+
+ /**
+ * This subclass is only used internally by Smack.
+ */
+ @SuppressWarnings("JavaLangClash")
+ static final class Error extends AdHocCommandResult {
+ private Error(AdHocCommandData response) {
+ super(response, false);
+ }
+ }
+
+ public static AdHocCommandResult from(AdHocCommandData response) {
+ IQ.Type iqType = response.getType();
+ if (iqType == IQ.Type.error)
+ return new Error(response);
+
+ assert iqType == IQ.Type.result;
+
+ switch (response.getStatus()) {
+ case executing:
+ return new StatusExecuting(response);
+ case completed:
+ return new StatusCompleted(response);
+ default:
+ throw new IllegalArgumentException();
+ }
+ }
+}
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/LocalCommand.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/LocalCommand.java
deleted file mode 100755
index 1dd01d3ab..000000000
--- a/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/LocalCommand.java
+++ /dev/null
@@ -1,168 +0,0 @@
-/**
- *
- * Copyright 2005-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.commands;
-
-import org.jivesoftware.smackx.commands.packet.AdHocCommandData;
-
-import org.jxmpp.jid.Jid;
-
-/**
- * Represents a command that can be executed locally from a remote location. This
- * class must be extended to implement an specific ad-hoc command. This class
- * provides some useful tools:
- * - Node
- * - Name
- * - Session ID
- * - Current Stage
- * - Available actions
- * - Default action
- *
- * To implement a new command extend this class and implement all the abstract
- * methods. When implementing the actions remember that they could be invoked
- * several times, and that you must use the current stage number to know what to
- * do.
- *
- * @author Gabriel Guardincerri
- */
-public abstract class LocalCommand extends AdHocCommand {
-
- /**
- * The time stamp of first invocation of the command. Used to implement the session timeout.
- */
- private final long creationDate;
-
- /**
- * The unique ID of the execution of the command.
- */
- private String sessionID;
-
- /**
- * The full JID of the host of the command.
- */
- private Jid ownerJID;
-
- /**
- * The number of the current stage.
- */
- private int currentStage;
-
- public LocalCommand() {
- super();
- this.creationDate = System.currentTimeMillis();
- currentStage = -1;
- }
-
- /**
- * The sessionID is an unique identifier of an execution request. This is
- * automatically handled and should not be called.
- *
- * @param sessionID the unique session id of this execution
- */
- public void setSessionID(String sessionID) {
- this.sessionID = sessionID;
- getData().setSessionID(sessionID);
- }
-
- /**
- * Returns the session ID of this execution.
- *
- * @return the unique session id of this execution
- */
- public String getSessionID() {
- return sessionID;
- }
-
- /**
- * Sets the JID of the command host. This is automatically handled and should
- * not be called.
- *
- * @param ownerJID the JID of the owner.
- */
- public void setOwnerJID(Jid ownerJID) {
- this.ownerJID = ownerJID;
- }
-
- @Override
- public Jid getOwnerJID() {
- return ownerJID;
- }
-
- /**
- * Returns the date the command was created.
- *
- * @return the date the command was created.
- */
- public long getCreationDate() {
- return creationDate;
- }
-
- /**
- * Returns true if the current stage is the last one. If it is then the
- * execution of some action will complete the execution of the command.
- * Commands that don't have multiple stages can always return true
.
- *
- * @return true if the command is in the last stage.
- */
- public abstract boolean isLastStage();
-
- /**
- * Returns true if the specified requester has permission to execute all the
- * stages of this action. This is checked when the first request is received,
- * if the permission is grant then the requester will be able to execute
- * all the stages of the command. It is not checked again during the
- * execution.
- *
- * @param jid the JID to check permissions on.
- * @return true if the user has permission to execute this action.
- */
- public abstract boolean hasPermission(Jid jid);
-
- /**
- * Returns the currently executing stage number. The first stage number is
- * 0. During the execution of the first action this method will answer 0.
- *
- * @return the current stage number.
- */
- public int getCurrentStage() {
- return currentStage;
- }
-
- @Override
- void setData(AdHocCommandData data) {
- data.setSessionID(sessionID);
- super.setData(data);
- }
-
- /**
- * Increase the current stage number. This is automatically handled and should
- * not be called.
- *
- */
- void incrementStage() {
- currentStage++;
- }
-
- /**
- * Decrease the current stage number. This is automatically handled and should
- * not be called.
- *
- */
- void decrementStage() {
- currentStage--;
- }
-}
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
deleted file mode 100755
index e79919eb6..000000000
--- a/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/RemoteCommand.java
+++ /dev/null
@@ -1,164 +0,0 @@
-/**
- *
- * Copyright 2005-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.commands;
-
-import org.jivesoftware.smack.SmackException.NoResponseException;
-import org.jivesoftware.smack.SmackException.NotConnectedException;
-import org.jivesoftware.smack.XMPPConnection;
-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.FillableForm;
-import org.jivesoftware.smackx.xdata.packet.DataForm;
-
-import org.jxmpp.jid.Jid;
-
-/**
- * Represents a command that is in a remote location. Invoking one of the
- * {@link AdHocCommand.Action#execute execute}, {@link AdHocCommand.Action#next next},
- * {@link AdHocCommand.Action#prev prev}, {@link AdHocCommand.Action#cancel cancel} or
- * {@link AdHocCommand.Action#complete complete} actions results in executing that
- * action in the remote location. In response to that action the internal state
- * of the this command instance will change. For example, if the command is a
- * single stage command, then invoking the execute action will execute this
- * action in the remote location. After that the local instance will have a
- * state of "completed" and a form or notes that applies.
- *
- * @author Gabriel Guardincerri
- *
- */
-public class RemoteCommand extends AdHocCommand {
-
- /**
- * The connection that is used to execute this command
- */
- private final XMPPConnection connection;
-
- /**
- * The full JID of the command host
- */
- private final Jid jid;
-
- /**
- * The session ID of this execution.
- */
- private String sessionID;
-
- /**
- * Creates a new RemoteCommand that uses an specific connection to execute a
- * command identified by node
in the host identified by
- * jid
- *
- * @param connection the connection to use for the execution.
- * @param node the identifier of the command.
- * @param jid the JID of the host.
- */
- protected RemoteCommand(XMPPConnection connection, String node, Jid jid) {
- super();
- this.connection = connection;
- this.jid = jid;
- this.setNode(node);
- }
-
- @Override
- public void cancel() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
- executeAction(Action.cancel);
- }
-
- @Override
- public void complete(FillableForm form) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
- executeAction(Action.complete, form.getDataFormToSubmit());
- }
-
- @Override
- public void execute() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
- executeAction(Action.execute);
- }
-
- /**
- * Executes the default action of the command with the information provided
- * in the Form. This form must be the answer form of the previous stage. If
- * there is a problem executing the command it throws an XMPPException.
- *
- * @param form the form answer of the previous stage.
- * @throws XMPPErrorException if an error occurs.
- * @throws NoResponseException if there was no response from the server.
- * @throws NotConnectedException if the XMPP connection is not connected.
- * @throws InterruptedException if the calling thread was interrupted.
- */
- public void execute(FillableForm form) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
- executeAction(Action.execute, form.getDataFormToSubmit());
- }
-
- @Override
- public void next(FillableForm form) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
- executeAction(Action.next, form.getDataFormToSubmit());
- }
-
- @Override
- public void prev() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
- executeAction(Action.prev);
- }
-
- private void executeAction(Action action) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
- executeAction(action, null);
- }
-
- /**
- * Executes the action
with the form
.
- * The action could be any of the available actions. The form must
- * be the answer of the previous stage. It can be null
if it is the first stage.
- *
- * @param action the action to execute.
- * @param form the form with the information.
- * @throws XMPPErrorException if there is a problem executing the command.
- * @throws NoResponseException if there was no response from the server.
- * @throws NotConnectedException if the XMPP connection is not connected.
- * @throws InterruptedException if the calling thread was interrupted.
- */
- 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.
- AdHocCommandData data = new AdHocCommandData();
- data.setType(IQ.Type.set);
- data.setTo(getOwnerJID());
- data.setNode(getNode());
- data.setSessionID(sessionID);
- data.setAction(action);
- data.setForm(form);
-
- AdHocCommandData responseData = null;
- try {
- responseData = connection.sendIqRequestAndWaitForResponse(data);
- }
- finally {
- // We set the response data in a 'finally' block, so that it also gets set even if an error IQ was returned.
- if (responseData != null) {
- this.sessionID = responseData.getSessionID();
- super.setData(responseData);
- }
- }
-
- }
-
- @Override
- public Jid getOwnerJID() {
- return jid;
- }
-}
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/SpecificErrorCondition.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/SpecificErrorCondition.java
new file mode 100644
index 000000000..fcf94a989
--- /dev/null
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/SpecificErrorCondition.java
@@ -0,0 +1,63 @@
+/**
+ *
+ * Copyright 2005-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.commands;
+
+public enum SpecificErrorCondition {
+
+ /**
+ * The responding JID cannot accept the specified action.
+ */
+ badAction("bad-action"),
+
+ /**
+ * The responding JID does not understand the specified action.
+ */
+ malformedAction("malformed-action"),
+
+ /**
+ * The responding JID cannot accept the specified language/locale.
+ */
+ badLocale("bad-locale"),
+
+ /**
+ * The responding JID cannot accept the specified payload (e.g. the data
+ * form did not provide one or more required fields).
+ */
+ badPayload("bad-payload"),
+
+ /**
+ * The responding JID cannot accept the specified sessionid.
+ */
+ badSessionid("bad-sessionid"),
+
+ /**
+ * The requesting JID specified a sessionid that is no longer active
+ * (either because it was completed, canceled, or timed out).
+ */
+ sessionExpired("session-expired");
+
+ private final String value;
+
+ SpecificErrorCondition(String value) {
+ this.value = value;
+ }
+
+ @Override
+ public String toString() {
+ return value;
+ }
+}
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/packet/AdHocCommandData.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/packet/AdHocCommandData.java
index f5681b0fc..19f29ac9b 100755
--- a/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/packet/AdHocCommandData.java
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/packet/AdHocCommandData.java
@@ -18,63 +18,114 @@
package org.jivesoftware.smackx.commands.packet;
import java.util.ArrayList;
+import java.util.EnumSet;
import java.util.List;
+import java.util.Set;
+import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.IqData;
import org.jivesoftware.smack.packet.XmlElement;
-import org.jivesoftware.smackx.commands.AdHocCommand;
-import org.jivesoftware.smackx.commands.AdHocCommand.Action;
-import org.jivesoftware.smackx.commands.AdHocCommand.SpecificErrorCondition;
import org.jivesoftware.smackx.commands.AdHocCommandNote;
+import org.jivesoftware.smackx.commands.SpecificErrorCondition;
import org.jivesoftware.smackx.xdata.packet.DataForm;
-import org.jxmpp.jid.Jid;
-
/**
* Represents the state and the request of the execution of an adhoc command.
*
* @author Gabriel Guardincerri
+ * @author Florian Schmaus
*/
-public class AdHocCommandData extends IQ {
+public class AdHocCommandData extends IQ implements AdHocCommandDataView {
public static final String ELEMENT = "command";
public static final String NAMESPACE = "http://jabber.org/protocol/commands";
- /* JID of the command host */
- private Jid id;
+ private final String node;
- /* Command name */
- private String name;
+ private final String name;
- /* Command identifier */
- private String node;
-
- /* Unique ID of the execution */
- private String sessionID;
+ private final String sessionId;
private final List notes = new ArrayList<>();
- private DataForm form;
+ private final DataForm form;
- /* Action request to be executed */
- private AdHocCommand.Action action;
+ private final Action action;
- /* Current execution status */
- private AdHocCommand.Status status;
+ private final Status status;
- private final ArrayList actions = new ArrayList<>();
+ private final Set actions = EnumSet.noneOf(AllowedAction.class);
- private AdHocCommand.Action executeAction;
+ private final AllowedAction executeAction;
- public AdHocCommandData() {
- super(ELEMENT, NAMESPACE);
+ public AdHocCommandData(AdHocCommandDataBuilder builder) {
+ super(builder, ELEMENT, NAMESPACE);
+ node = builder.getNode();
+ name = builder.getName();
+ sessionId = builder.getSessionId();
+ notes.addAll(builder.getNotes());
+ form = builder.getForm();
+ action = builder.getAction();
+ status = builder.getStatus();
+ actions.addAll(builder.getActions());
+ executeAction = builder.getExecuteAction();
+
+ if (executeAction != null && !actions.contains(executeAction)) {
+ throw new IllegalArgumentException("Execute action " + executeAction + " is not part of allowed actions: " + actions);
+ }
+ }
+
+ @Override
+ public String getNode() {
+ return node;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String getSessionId() {
+ return sessionId;
+ }
+
+ @Override
+ public List getNotes() {
+ return notes;
+ }
+
+ @Override
+ public DataForm getForm() {
+ return form;
+ }
+
+ @Override
+ public Action getAction() {
+ return action;
+ }
+
+ @Override
+ public Status getStatus() {
+ return status;
+ }
+
+ @Override
+ public Set getActions() {
+ return actions;
+ }
+
+ @Override
+ public AllowedAction getExecuteAction() {
+ return executeAction;
}
@Override
protected IQChildElementXmlStringBuilder getIQChildElementBuilder(IQChildElementXmlStringBuilder xml) {
xml.attribute("node", node);
- xml.optAttribute("sessionid", sessionID);
+ xml.optAttribute("sessionid", sessionId);
xml.optAttribute("status", status);
xml.optAttribute("action", action);
xml.rightAngleBracket();
@@ -87,19 +138,19 @@ public class AdHocCommandData extends IQ {
} else {
xml.rightAngleBracket();
- for (AdHocCommand.Action action : actions) {
+ for (AdHocCommandData.AllowedAction action : actions) {
xml.emptyElement(action);
}
xml.closeElement("actions");
}
}
- if (form != null) {
- xml.append(form.toXML());
- }
+ xml.optAppend(form);
for (AdHocCommandNote note : notes) {
- xml.halfOpenElement("note").attribute("type", note.getType().toString()).rightAngleBracket();
+ xml.halfOpenElement("note")
+ .attribute("type", note.getType().toString())
+ .rightAngleBracket();
xml.append(note.getValue());
xml.closeElement("note");
}
@@ -112,132 +163,16 @@ public class AdHocCommandData extends IQ {
return xml;
}
- /**
- * Returns the JID of the command host.
- *
- * @return the JID of the command host.
- */
- public Jid getId() {
- return id;
+ public static AdHocCommandDataBuilder builder(String node, IqData iqCommon) {
+ return new AdHocCommandDataBuilder(node, iqCommon);
}
- public void setId(Jid id) {
- this.id = id;
+ public static AdHocCommandDataBuilder builder(String node, String stanzaId) {
+ return new AdHocCommandDataBuilder(node, stanzaId);
}
- /**
- * Returns the human name of the command.
- *
- * @return the name of the command.
- */
- public String getName() {
- return name;
- }
-
- public void setName(String name) {
- this.name = name;
- }
-
- /**
- * Returns the identifier of the command.
- *
- * @return the node.
- */
- public String getNode() {
- return node;
- }
-
- public void setNode(String node) {
- this.node = node;
- }
-
- /**
- * Returns the list of notes that the command has.
- *
- * @return the notes.
- */
- public List getNotes() {
- return notes;
- }
-
- public void addNote(AdHocCommandNote note) {
- this.notes.add(note);
- }
-
- public void removeNote(AdHocCommandNote note) {
- this.notes.remove(note);
- }
-
- /**
- * Returns the form of the command.
- *
- * @return the data form associated with the command.
- */
- public DataForm getForm() {
- return form;
- }
-
- public void setForm(DataForm form) {
- this.form = form;
- }
-
- /**
- * Returns the action to execute. The action is set only on a request.
- *
- * @return the action to execute.
- */
- public AdHocCommand.Action getAction() {
- return action;
- }
-
- public void setAction(AdHocCommand.Action action) {
- this.action = action;
- }
-
- /**
- * Returns the status of the execution.
- *
- * @return the status.
- */
- public AdHocCommand.Status getStatus() {
- return status;
- }
-
- public void setStatus(AdHocCommand.Status status) {
- this.status = status;
- }
-
- public List getActions() {
- return actions;
- }
-
- public void addAction(Action action) {
- actions.add(action);
- }
-
- public void setExecuteAction(Action executeAction) {
- this.executeAction = executeAction;
- }
-
- public Action getExecuteAction() {
- return executeAction;
- }
-
- /**
- * Set the 'sessionid' attribute of the command.
- *
- * This value can be null or empty for the first command, but MUST be set for subsequent commands. See also XEP-0050 § 3.3 Session Lifetime.
- *
- *
- * @param sessionID TODO javadoc me please
- */
- public void setSessionID(String sessionID) {
- this.sessionID = sessionID;
- }
-
- public String getSessionID() {
- return sessionID;
+ public static AdHocCommandDataBuilder builder(String node, XMPPConnection connection) {
+ return new AdHocCommandDataBuilder(node, connection);
}
public static class SpecificError implements XmlElement {
@@ -271,4 +206,86 @@ public class AdHocCommandData extends IQ {
return buf.toString();
}
}
+
+ /**
+ * The status of the stage in the adhoc command.
+ */
+ public enum Status {
+
+ /**
+ * The command is being executed.
+ */
+ executing,
+
+ /**
+ * The command has completed. The command session has ended.
+ */
+ completed,
+
+ /**
+ * The command has been canceled. The command session has ended.
+ */
+ canceled
+ }
+
+ public enum AllowedAction {
+
+ /**
+ * The command should be digress to the previous stage of execution.
+ */
+ prev(Action.prev),
+
+ /**
+ * The command should progress to the next stage of execution.
+ */
+ next(Action.next),
+
+ /**
+ * The command should be completed (if possible).
+ */
+ complete(Action.complete),
+ ;
+
+ public final Action action;
+
+ AllowedAction(Action action) {
+ this.action = action;
+ }
+ }
+
+ public enum Action {
+ /**
+ * The command should be executed or continue to be executed. This is
+ * the default value.
+ */
+ execute(null),
+
+ /**
+ * The command should be canceled.
+ */
+ cancel(null),
+
+ /**
+ * The command should be digress to the previous stage of execution.
+ */
+ prev(AllowedAction.prev),
+
+ /**
+ * The command should progress to the next stage of execution.
+ */
+ next(AllowedAction.next),
+
+ /**
+ * The command should be completed (if possible).
+ */
+ complete(AllowedAction.complete),
+ ;
+
+ public final AllowedAction allowedAction;
+
+ Action(AllowedAction allowedAction) {
+ this.allowedAction = allowedAction;
+ }
+
+ }
}
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/packet/AdHocCommandDataBuilder.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/packet/AdHocCommandDataBuilder.java
new file mode 100644
index 000000000..fea5e6259
--- /dev/null
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/packet/AdHocCommandDataBuilder.java
@@ -0,0 +1,220 @@
+/**
+ *
+ * Copyright 2023 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.commands.packet;
+
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Set;
+
+import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smack.packet.AbstractIqBuilder;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.IqBuilder;
+import org.jivesoftware.smack.packet.IqData;
+import org.jivesoftware.smack.util.StringUtils;
+
+import org.jivesoftware.smackx.commands.AdHocCommandNote;
+import org.jivesoftware.smackx.commands.packet.AdHocCommandData.Action;
+import org.jivesoftware.smackx.commands.packet.AdHocCommandData.AllowedAction;
+import org.jivesoftware.smackx.commands.packet.AdHocCommandData.Status;
+import org.jivesoftware.smackx.xdata.packet.DataForm;
+
+public class AdHocCommandDataBuilder extends IqBuilder implements AdHocCommandDataView {
+
+ private final String node;
+
+ private String name;
+
+ private String sessionId;
+
+ private final List notes = new ArrayList<>();
+
+ private DataForm form;
+
+ /* Action request to be executed */
+ private Action action;
+
+ /* Current execution status */
+ private Status status;
+
+ private final Set actions = EnumSet.noneOf(AllowedAction.class);
+
+ private AllowedAction executeAction;
+
+ AdHocCommandDataBuilder(String node, IqData iqCommon) {
+ super(iqCommon);
+ this.node = StringUtils.requireNotNullNorEmpty(node, "Ad-Hoc Command node must be set");
+ }
+
+ AdHocCommandDataBuilder(String node, String stanzaId) {
+ super(stanzaId);
+ this.node = StringUtils.requireNotNullNorEmpty(node, "Ad-Hoc Command node must be set");
+ }
+
+ AdHocCommandDataBuilder(String node, XMPPConnection connection) {
+ super(connection);
+ this.node = StringUtils.requireNotNullNorEmpty(node, "Ad-Hoc Command node must be set");
+ }
+
+ @Override
+ public String getNode() {
+ return node;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ public AdHocCommandDataBuilder setName(String name) {
+ this.name = name;
+ return getThis();
+ }
+
+ @Override
+ public String getSessionId() {
+ return sessionId;
+ }
+
+ public AdHocCommandDataBuilder setSessionId(String sessionId) {
+ this.sessionId = sessionId;
+ return getThis();
+ }
+
+ @Override
+ public List getNotes() {
+ return notes;
+ }
+
+ public AdHocCommandDataBuilder addNote(AdHocCommandNote note) {
+ notes.add(note);
+ return getThis();
+ }
+
+ @Override
+ public DataForm getForm() {
+ return form;
+ }
+
+ public AdHocCommandDataBuilder setForm(DataForm form) {
+ this.form = form;
+ return getThis();
+ }
+
+ @Override
+ public Action getAction() {
+ return action;
+ }
+
+ public AdHocCommandDataBuilder setAction(AdHocCommandData.Action action) {
+ this.action = action;
+ return getThis();
+ }
+ @Override
+
+ public AdHocCommandData.Status getStatus() {
+ return status;
+ }
+
+ public AdHocCommandDataBuilder setStatus(AdHocCommandData.Status status) {
+ this.status = status;
+ return getThis();
+ }
+
+ public AdHocCommandDataBuilder setStatusCompleted() {
+ return setStatus(AdHocCommandData.Status.completed);
+ }
+
+ public enum PreviousStage {
+ exists,
+ none,
+ }
+
+ public enum NextStage {
+ isFinal,
+ nonFinal,
+ }
+
+ @SuppressWarnings("fallthrough")
+ public AdHocCommandDataBuilder setStatusExecuting(PreviousStage previousStage, NextStage nextStage) {
+ setStatus(AdHocCommandData.Status.executing);
+
+ switch (previousStage) {
+ case exists:
+ addAction(AllowedAction.prev);
+ break;
+ case none:
+ break;
+ }
+
+ setExecuteAction(AllowedAction.next);
+
+ switch (nextStage) {
+ case isFinal:
+ addAction(AllowedAction.complete);
+ // Override execute action of 'next'.
+ setExecuteAction(AllowedAction.complete);
+ // Deliberate fallthrough, we want 'next' to be added.
+ case nonFinal:
+ addAction(AllowedAction.next);
+ break;
+ }
+
+ return getThis();
+ }
+
+ @Override
+ public Set getActions() {
+ return actions;
+ }
+
+ public AdHocCommandDataBuilder addAction(AllowedAction action) {
+ actions.add(action);
+ return getThis();
+ }
+
+ @Override
+ public AllowedAction getExecuteAction() {
+ return executeAction;
+ }
+
+ public AdHocCommandDataBuilder setExecuteAction(AllowedAction action) {
+ this.executeAction = action;
+ return getThis();
+ }
+
+ @Override
+ public AdHocCommandData build() {
+ return new AdHocCommandData(this);
+ }
+
+ @Override
+ public AdHocCommandDataBuilder getThis() {
+ return this;
+ }
+
+ public static AdHocCommandDataBuilder buildResponseFor(AdHocCommandData request) {
+ return buildResponseFor(request, IQ.ResponseType.result);
+ }
+
+ public static AdHocCommandDataBuilder buildResponseFor(AdHocCommandData request, IQ.ResponseType responseType) {
+ AdHocCommandDataBuilder builder = new AdHocCommandDataBuilder(request.getNode(), AbstractIqBuilder.createResponse(request, responseType));
+ return builder;
+ }
+
+}
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/packet/AdHocCommandDataView.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/packet/AdHocCommandDataView.java
new file mode 100644
index 000000000..f1cc4b0ca
--- /dev/null
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/packet/AdHocCommandDataView.java
@@ -0,0 +1,87 @@
+/**
+ *
+ * Copyright 2023 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.commands.packet;
+
+import java.util.List;
+import java.util.Set;
+
+import org.jivesoftware.smack.packet.IqView;
+
+import org.jivesoftware.smackx.commands.AdHocCommandNote;
+import org.jivesoftware.smackx.commands.packet.AdHocCommandData.Action;
+import org.jivesoftware.smackx.commands.packet.AdHocCommandData.AllowedAction;
+import org.jivesoftware.smackx.commands.packet.AdHocCommandData.Status;
+import org.jivesoftware.smackx.xdata.packet.DataForm;
+
+public interface AdHocCommandDataView extends IqView {
+
+ /**
+ * Returns the identifier of the command.
+ *
+ * @return the node.
+ */
+ String getNode();
+
+ /**
+ * Returns the human name of the command.
+ *
+ * @return the name of the command.
+ */
+ String getName();
+
+ String getSessionId();
+
+ /**
+ * Returns the list of notes that the command has.
+ *
+ * @return the notes.
+ */
+ List getNotes();
+
+ /**
+ * Returns the form of the command.
+ *
+ * @return the data form associated with the command.
+ */
+ DataForm getForm();
+
+ /**
+ * Returns the action to execute. The action is set only on a request.
+ *
+ * @return the action to execute.
+ */
+ Action getAction();
+
+ /**
+ * Returns the status of the execution.
+ *
+ * @return the status.
+ */
+ Status getStatus();
+
+ Set getActions();
+
+ AllowedAction getExecuteAction();
+
+ default boolean isCompleted() {
+ return getStatus() == Status.completed;
+ }
+
+ default boolean isExecuting() {
+ return getStatus() == Status.executing;
+ }
+}
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/provider/AdHocCommandDataProvider.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/provider/AdHocCommandDataProvider.java
index 26dfa56a3..6f9b75477 100755
--- a/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/provider/AdHocCommandDataProvider.java
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/provider/AdHocCommandDataProvider.java
@@ -29,10 +29,13 @@ import org.jivesoftware.smack.util.PacketParserUtils;
import org.jivesoftware.smack.xml.XmlPullParser;
import org.jivesoftware.smack.xml.XmlPullParserException;
-import org.jivesoftware.smackx.commands.AdHocCommand;
-import org.jivesoftware.smackx.commands.AdHocCommand.Action;
import org.jivesoftware.smackx.commands.AdHocCommandNote;
+import org.jivesoftware.smackx.commands.SpecificErrorCondition;
import org.jivesoftware.smackx.commands.packet.AdHocCommandData;
+import org.jivesoftware.smackx.commands.packet.AdHocCommandData.Action;
+import org.jivesoftware.smackx.commands.packet.AdHocCommandData.AllowedAction;
+import org.jivesoftware.smackx.commands.packet.AdHocCommandDataBuilder;
+import org.jivesoftware.smackx.xdata.packet.DataForm;
import org.jivesoftware.smackx.xdata.provider.DataFormProvider;
/**
@@ -44,64 +47,69 @@ public class AdHocCommandDataProvider extends IqProvider {
@Override
public AdHocCommandData parse(XmlPullParser parser, int initialDepth, IqData iqData, XmlEnvironment xmlEnvironment) throws XmlPullParserException, IOException, SmackParsingException {
- boolean done = false;
- AdHocCommandData adHocCommandData = new AdHocCommandData();
+ String commandNode = parser.getAttributeValue("node");
+ AdHocCommandDataBuilder builder = AdHocCommandData.builder(commandNode, iqData);
DataFormProvider dataFormProvider = new DataFormProvider();
- XmlPullParser.Event eventType;
- String elementName;
- String namespace;
- adHocCommandData.setSessionID(parser.getAttributeValue("", "sessionid"));
- adHocCommandData.setNode(parser.getAttributeValue("", "node"));
+ String sessionId = parser.getAttributeValue("sessionid");
+ builder.setSessionId(sessionId);
// Status
String status = parser.getAttributeValue("", "status");
- if (AdHocCommand.Status.executing.toString().equalsIgnoreCase(status)) {
- adHocCommandData.setStatus(AdHocCommand.Status.executing);
+ if (AdHocCommandData.Status.executing.toString().equalsIgnoreCase(status)) {
+ builder.setStatus(AdHocCommandData.Status.executing);
}
- else if (AdHocCommand.Status.completed.toString().equalsIgnoreCase(status)) {
- adHocCommandData.setStatus(AdHocCommand.Status.completed);
+ else if (AdHocCommandData.Status.completed.toString().equalsIgnoreCase(status)) {
+ builder.setStatus(AdHocCommandData.Status.completed);
}
- else if (AdHocCommand.Status.canceled.toString().equalsIgnoreCase(status)) {
- adHocCommandData.setStatus(AdHocCommand.Status.canceled);
+ else if (AdHocCommandData.Status.canceled.toString().equalsIgnoreCase(status)) {
+ builder.setStatus(AdHocCommandData.Status.canceled);
}
// Action
String action = parser.getAttributeValue("", "action");
if (action != null) {
- Action realAction = AdHocCommand.Action.valueOf(action);
- if (realAction == null || realAction.equals(Action.unknown)) {
- adHocCommandData.setAction(Action.unknown);
- }
- else {
- adHocCommandData.setAction(realAction);
+ Action realAction = Action.valueOf(action);
+ if (realAction == null) {
+ throw new SmackParsingException("Invalid value for action attribute: " + action);
}
+
+ builder.setAction(realAction);
}
- while (!done) {
- eventType = parser.next();
- namespace = parser.getNamespace();
- if (eventType == XmlPullParser.Event.START_ELEMENT) {
+
+ // TODO: Improve parsing below. Currently, the next actions like are not checked for the correct position.
+ outerloop:
+ while (true) {
+ String elementName;
+ XmlPullParser.Event event = parser.next();
+ String namespace = parser.getNamespace();
+ switch (event) {
+ case START_ELEMENT:
elementName = parser.getName();
- if (parser.getName().equals("actions")) {
- String execute = parser.getAttributeValue("", "execute");
+ switch (elementName) {
+ case "actions":
+ String execute = parser.getAttributeValue("execute");
if (execute != null) {
- adHocCommandData.setExecuteAction(AdHocCommand.Action.valueOf(execute));
+ builder.setExecuteAction(AllowedAction.valueOf(execute));
}
- }
- else if (parser.getName().equals("next")) {
- adHocCommandData.addAction(AdHocCommand.Action.next);
- }
- else if (parser.getName().equals("complete")) {
- adHocCommandData.addAction(AdHocCommand.Action.complete);
- }
- else if (parser.getName().equals("prev")) {
- adHocCommandData.addAction(AdHocCommand.Action.prev);
- }
- else if (elementName.equals("x") && namespace.equals("jabber:x:data")) {
- adHocCommandData.setForm(dataFormProvider.parse(parser));
- }
- else if (parser.getName().equals("note")) {
- String typeString = parser.getAttributeValue("", "type");
+ break;
+ case "next":
+ builder.addAction(AllowedAction.next);
+ break;
+ case "complete":
+ builder.addAction(AllowedAction.complete);
+ break;
+ case "prev":
+ builder.addAction(AllowedAction.prev);
+ break;
+ case "x":
+ if (namespace.equals("jabber:x:data")) {
+ DataForm form = dataFormProvider.parse(parser);
+ builder.setForm(form);
+ }
+ break;
+ case "note":
+ String typeString = parser.getAttributeValue("type");
AdHocCommandNote.Type type;
if (typeString != null) {
type = AdHocCommandNote.Type.valueOf(typeString);
@@ -110,61 +118,67 @@ public class AdHocCommandDataProvider extends IqProvider {
type = AdHocCommandNote.Type.info;
}
String value = parser.nextText();
- adHocCommandData.addNote(new AdHocCommandNote(type, value));
- }
- else if (parser.getName().equals("error")) {
+ builder.addNote(new AdHocCommandNote(type, value));
+ break;
+ case "error":
StanzaError error = PacketParserUtils.parseError(parser);
- adHocCommandData.setError(error);
+ builder.setError(error);
+ break;
}
- }
- else if (eventType == XmlPullParser.Event.END_ELEMENT) {
+ break;
+ case END_ELEMENT:
if (parser.getName().equals("command")) {
- done = true;
+ break outerloop;
}
+ break;
+ default:
+ // Catch all for incomplete switch (MissingCasesInEnumSwitch) statement.
+ break;
}
}
- return adHocCommandData;
+
+ return builder.build();
}
public static class BadActionError extends ExtensionElementProvider {
@Override
public AdHocCommandData.SpecificError parse(XmlPullParser parser, int initialDepth, XmlEnvironment xmlEnvironment) {
- return new AdHocCommandData.SpecificError(AdHocCommand.SpecificErrorCondition.badAction);
+ return new AdHocCommandData.SpecificError(SpecificErrorCondition.badAction);
}
}
public static class MalformedActionError extends ExtensionElementProvider {
@Override
public AdHocCommandData.SpecificError parse(XmlPullParser parser, int initialDepth, XmlEnvironment xmlEnvironment) {
- return new AdHocCommandData.SpecificError(AdHocCommand.SpecificErrorCondition.malformedAction);
+ return new AdHocCommandData.SpecificError(SpecificErrorCondition.malformedAction);
}
}
public static class BadLocaleError extends ExtensionElementProvider {
@Override
public AdHocCommandData.SpecificError parse(XmlPullParser parser, int initialDepth, XmlEnvironment xmlEnvironment) {
- return new AdHocCommandData.SpecificError(AdHocCommand.SpecificErrorCondition.badLocale);
+ return new AdHocCommandData.SpecificError(SpecificErrorCondition.badLocale);
}
}
public static class BadPayloadError extends ExtensionElementProvider {
@Override
public AdHocCommandData.SpecificError parse(XmlPullParser parser, int initialDepth, XmlEnvironment xmlEnvironment) {
- return new AdHocCommandData.SpecificError(AdHocCommand.SpecificErrorCondition.badPayload);
+ return new AdHocCommandData.SpecificError(SpecificErrorCondition.badPayload);
}
}
public static class BadSessionIDError extends ExtensionElementProvider {
@Override
public AdHocCommandData.SpecificError parse(XmlPullParser parser, int initialDepth, XmlEnvironment xmlEnvironment) {
- return new AdHocCommandData.SpecificError(AdHocCommand.SpecificErrorCondition.badSessionid);
+ return new AdHocCommandData.SpecificError(SpecificErrorCondition.badSessionid);
}
}
public static class SessionExpiredError extends ExtensionElementProvider {
@Override
public AdHocCommandData.SpecificError parse(XmlPullParser parser, int initialDepth, XmlEnvironment xmlEnvironment) {
- return new AdHocCommandData.SpecificError(AdHocCommand.SpecificErrorCondition.sessionExpired);
+ return new AdHocCommandData.SpecificError(SpecificErrorCondition.sessionExpired);
}
}
}
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/DirectMucInvitationListener.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/DirectMucInvitationListener.java
new file mode 100644
index 000000000..e8ef1af5e
--- /dev/null
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/DirectMucInvitationListener.java
@@ -0,0 +1,25 @@
+/**
+ *
+ * Copyright 2020 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.muc;
+
+import org.jivesoftware.smack.packet.Stanza;
+import org.jivesoftware.smackx.muc.packet.GroupChatInvitation;
+
+public interface DirectMucInvitationListener {
+
+ void invitationReceived(GroupChatInvitation invitation, Stanza stanza);
+}
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/DirectMucInvitationManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/DirectMucInvitationManager.java
new file mode 100644
index 000000000..b05f10df0
--- /dev/null
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/DirectMucInvitationManager.java
@@ -0,0 +1,111 @@
+/**
+ *
+ * Copyright 2020 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.muc;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.WeakHashMap;
+
+import org.jivesoftware.smack.Manager;
+import org.jivesoftware.smack.SmackException;
+import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smack.XMPPConnectionRegistry;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.filter.StanzaExtensionFilter;
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smack.packet.MessageBuilder;
+import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
+import org.jivesoftware.smackx.muc.packet.GroupChatInvitation;
+
+import org.jxmpp.jid.EntityBareJid;
+
+/**
+ * Smacks API for XEP-0249: Direct MUC Invitations.
+ * Use this instead of {@link org.jivesoftware.smackx.muc.packet.MUCUser.Invite}.
+ *
+ * To invite a user to a group chat, use {@link #inviteToMuc(MultiUserChat, EntityBareJid)}.
+ *
+ * In order to listen for incoming invitations, register a {@link DirectMucInvitationListener} using
+ * {@link #addInvitationListener(DirectMucInvitationListener)}.
+ *
+ * @see Direct MUC Invitations
+ */
+public final class DirectMucInvitationManager extends Manager {
+
+ private static final Map INSTANCES = new WeakHashMap<>();
+ private final List directMucInvitationListeners = new ArrayList<>();
+ private final ServiceDiscoveryManager serviceDiscoveryManager;
+
+ static {
+ XMPPConnectionRegistry.addConnectionCreationListener(DirectMucInvitationManager::getInstanceFor);
+ }
+
+ public static synchronized DirectMucInvitationManager getInstanceFor(XMPPConnection connection) {
+ DirectMucInvitationManager manager = INSTANCES.get(connection);
+ if (manager == null) {
+ manager = new DirectMucInvitationManager(connection);
+ INSTANCES.put(connection, manager);
+ }
+ return manager;
+ }
+
+ private DirectMucInvitationManager(XMPPConnection connection) {
+ super(connection);
+ serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection);
+
+ connection().addAsyncStanzaListener(stanza -> {
+ GroupChatInvitation invitation = stanza.getExtension(GroupChatInvitation.class);
+ for (DirectMucInvitationListener listener : directMucInvitationListeners) {
+ listener.invitationReceived(invitation, stanza);
+ }
+ }, new StanzaExtensionFilter(GroupChatInvitation.ELEMENT, GroupChatInvitation.NAMESPACE));
+ serviceDiscoveryManager.addFeature(GroupChatInvitation.NAMESPACE);
+ }
+
+ public void inviteToMuc(MultiUserChat muc, EntityBareJid user)
+ throws SmackException.NotConnectedException, InterruptedException {
+ inviteToMuc(muc, user, null, null, false, null);
+ }
+
+ public void inviteToMuc(MultiUserChat muc, EntityBareJid user, String password, String reason, boolean continueAsOneToOneChat, String thread)
+ throws SmackException.NotConnectedException, InterruptedException {
+ inviteToMuc(user, new GroupChatInvitation(muc.getRoom(), reason, password, continueAsOneToOneChat, thread));
+ }
+
+ public void inviteToMuc(EntityBareJid jid, GroupChatInvitation invitation) throws SmackException.NotConnectedException, InterruptedException {
+ Message invitationMessage = MessageBuilder.buildMessage()
+ .to(jid)
+ .addExtension(invitation)
+ .build();
+ connection().sendStanza(invitationMessage);
+ }
+
+ public boolean userSupportsInvitations(EntityBareJid jid)
+ throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
+ SmackException.NoResponseException {
+ return serviceDiscoveryManager.supportsFeature(jid, GroupChatInvitation.NAMESPACE);
+ }
+
+ public synchronized void addInvitationListener(DirectMucInvitationListener listener) {
+ directMucInvitationListeners.add(listener);
+ }
+
+ public synchronized void removeInvitationListener(DirectMucInvitationListener listener) {
+ directMucInvitationListeners.remove(listener);
+ }
+}
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatManager.java
index 00b088095..47de2163d 100644
--- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatManager.java
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatManager.java
@@ -83,6 +83,10 @@ import org.jxmpp.util.cache.ExpirationCache;
* further attempts will be made for the other rooms.
*
*
+ * Note:
+ * For inviting other users to a group chat or listening for such invitations, take a look at the
+ * {@link DirectMucInvitationManager} which provides an implementation of XEP-0249: Direct MUC Invitations.
+ *
* @see XEP-0045: Multi-User Chat
*/
public final class MultiUserChatManager extends Manager {
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/packet/GroupChatInvitation.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/packet/GroupChatInvitation.java
index dc6660ae9..e243d46ed 100644
--- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/packet/GroupChatInvitation.java
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/packet/GroupChatInvitation.java
@@ -1,6 +1,6 @@
/**
*
- * Copyright 2003-2007 Jive Software.
+ * Copyright 2003-2007 Jive Software, 2020 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,6 +20,8 @@ import javax.xml.namespace.QName;
import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.packet.Stanza;
+import org.jivesoftware.smack.util.EqualsUtil;
+import org.jivesoftware.smack.util.HashCode;
import org.jivesoftware.smack.util.Objects;
import org.jivesoftware.smack.util.XmlStringBuilder;
@@ -27,32 +29,13 @@ import org.jxmpp.jid.EntityBareJid;
/**
* A group chat invitation stanza extension, which is used to invite other
- * users to a group chat room. To invite a user to a group chat room, address
- * a new message to the user and set the room name appropriately, as in the
- * following code example:
+ * users to a group chat room.
*
- *
- * Message message = new Message("user@chat.example.com");
- * message.setBody("Join me for a group chat!");
- * message.addExtension(new GroupChatInvitation("room@chat.example.com"););
- * con.sendStanza(message);
- *
- *
- * To listen for group chat invitations, use a StanzaExtensionFilter for the
- * x
element name and jabber:x:conference
namespace, as in the
- * following code example:
- *
- *
- * PacketFilter filter = new StanzaExtensionFilter("x", "jabber:x:conference");
- * // Create a stanza collector or stanza listeners using the filter...
- *
- *
- * Note: this protocol is outdated now that the Multi-User Chat (MUC) XEP is available
- * (XEP-45). However, most
- * existing clients still use this older protocol. Once MUC support becomes more
- * widespread, this API may be deprecated.
+ * This implementation now conforms to XEP-0249: Direct MUC Invitations,
+ * while staying backwards compatible to legacy MUC invitations.
*
* @author Matt Tucker
+ * @author Paul Schaub
*/
public class GroupChatInvitation implements ExtensionElement {
@@ -68,6 +51,12 @@ public class GroupChatInvitation implements ExtensionElement {
public static final QName QNAME = new QName(NAMESPACE, ELEMENT);
+ public static final String ATTR_CONTINUE = "continue";
+ public static final String ATTR_JID = "jid";
+ public static final String ATTR_PASSWORD = "password";
+ public static final String ATTR_REASON = "reason";
+ public static final String ATTR_THREAD = "thread";
+
private final EntityBareJid roomAddress;
private final String reason;
private final String password;
@@ -170,18 +159,37 @@ public class GroupChatInvitation implements ExtensionElement {
@Override
public XmlStringBuilder toXML(org.jivesoftware.smack.packet.XmlEnvironment enclosingNamespace) {
XmlStringBuilder xml = new XmlStringBuilder(this);
- xml.attribute("jid", getRoomAddress());
- xml.optAttribute("reason", getReason());
- xml.optAttribute("password", getPassword());
- xml.optAttribute("thread", getThread());
-
- if (continueAsOneToOneChat())
- xml.optBooleanAttribute("continue", true);
+ xml.jidAttribute(getRoomAddress());
+ xml.optAttribute(ATTR_REASON, getReason());
+ xml.optAttribute(ATTR_PASSWORD, getPassword());
+ xml.optAttribute(ATTR_THREAD, getThread());
+ xml.optBooleanAttribute(ATTR_CONTINUE, continueAsOneToOneChat());
xml.closeEmptyElement();
return xml;
}
+ @Override
+ public boolean equals(Object obj) {
+ return EqualsUtil.equals(this, obj, (equalsBuilder, other) -> equalsBuilder
+ .append(getRoomAddress(), other.getRoomAddress())
+ .append(getPassword(), other.getPassword())
+ .append(getReason(), other.getReason())
+ .append(continueAsOneToOneChat(), other.continueAsOneToOneChat())
+ .append(getThread(), other.getThread()));
+ }
+
+ @Override
+ public int hashCode() {
+ return HashCode.builder()
+ .append(getRoomAddress())
+ .append(getPassword())
+ .append(getReason())
+ .append(continueAsOneToOneChat())
+ .append(getThread())
+ .build();
+ }
+
/**
* Get the group chat invitation from the given stanza.
* @param packet TODO javadoc me please
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/provider/GroupChatInvitationProvider.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/provider/GroupChatInvitationProvider.java
index 31f6472c8..00ddabaf9 100644
--- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/provider/GroupChatInvitationProvider.java
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/provider/GroupChatInvitationProvider.java
@@ -1,6 +1,6 @@
/**
*
- * Copyright 2003-2007 Jive Software, 2022 Florian Schmaus.
+ * Copyright 2003-2007 Jive Software, 2020 Paul Schaub, 2022-2023 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,8 +16,12 @@
*/
package org.jivesoftware.smackx.muc.provider;
+import static org.jivesoftware.smackx.muc.packet.GroupChatInvitation.ATTR_CONTINUE;
+import static org.jivesoftware.smackx.muc.packet.GroupChatInvitation.ATTR_PASSWORD;
+import static org.jivesoftware.smackx.muc.packet.GroupChatInvitation.ATTR_REASON;
+import static org.jivesoftware.smackx.muc.packet.GroupChatInvitation.ATTR_THREAD;
+
import java.io.IOException;
-import java.text.ParseException;
import org.jivesoftware.smack.packet.XmlEnvironment;
import org.jivesoftware.smack.parsing.SmackParsingException;
@@ -33,11 +37,14 @@ public class GroupChatInvitationProvider extends ExtensionElementProvider";
+
+ GroupChatInvitation invitation = new GroupChatInvitation(mucJid,
+ "Hey Hecate, this is the place for all good witches!",
+ "cauldronburn",
+ true,
+ "e0ffe42b28561960c6b12b944a092794b9683a38");
+ assertXmlSimilar(expectedXml, invitation.toXML());
+
+ GroupChatInvitation parsed = TEST_PROVIDER.parse(TestUtils.getParser(expectedXml));
+ assertEquals(invitation, parsed);
+ }
+
+ @Test
+ public void serializeMinimalElementTest() throws XmlPullParserException, IOException, SmackParsingException {
+ final String expectedXml = "";
+
+ GroupChatInvitation invitation = new GroupChatInvitation(mucJid);
+ assertXmlSimilar(expectedXml, invitation.toXML());
+
+ GroupChatInvitation parsed = TEST_PROVIDER.parse(TestUtils.getParser(expectedXml));
+ assertEquals(invitation, parsed);
+ }
+}
diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/xdata/form/FillableFormTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/xdata/form/FillableFormTest.java
new file mode 100644
index 000000000..1e495fe60
--- /dev/null
+++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/xdata/form/FillableFormTest.java
@@ -0,0 +1,44 @@
+/**
+ *
+ * Copyright 2024 Florian Schmaus.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.xdata.form;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.jivesoftware.smackx.xdata.FormField;
+import org.jivesoftware.smackx.xdata.packet.DataForm;
+
+import org.junit.jupiter.api.Test;
+
+public class FillableFormTest {
+
+ @Test
+ public void testThrowOnIncompleteyFilled() {
+ FormField fieldA = FormField.textSingleBuilder("a").setRequired().build();
+ FormField fieldB = FormField.textSingleBuilder("b").setRequired().build();
+ DataForm form = DataForm.builder(DataForm.Type.form)
+ .addField(fieldA)
+ .addField(fieldB)
+ .build();
+
+ FillableForm fillableForm = new FillableForm(form);
+ fillableForm.setAnswer("a", 42);
+
+ IllegalStateException ise = assertThrows(IllegalStateException.class, () -> fillableForm.getSubmitForm());
+ assertTrue(ise.getMessage().startsWith("Not all required fields filled. "));
+ }
+}
diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/XmppConnectionStressTest.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/XmppConnectionStressTest.java
index f9d988308..8afe9f9e4 100644
--- a/smack-integration-test/src/main/java/org/igniterealtime/smack/XmppConnectionStressTest.java
+++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/XmppConnectionStressTest.java
@@ -300,6 +300,7 @@ public class XmppConnectionStressTest {
Integer markerFromConnectionId = connectionIds.get(markerFromAddress);
sb.append(markerToConnectionId)
.append(" is missing ").append(numberOfFalseMarkers)
+ .append(" ( of ").append(marker.length).append(" messages)")
.append(" messages from ").append(markerFromConnectionId)
.append(": ");
for (int i = 0; i < marker.length; i++) {
diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/Configuration.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/Configuration.java
index 700c12eff..0df16141c 100644
--- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/Configuration.java
+++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/Configuration.java
@@ -1,6 +1,6 @@
/**
*
- * Copyright 2015-2021 Florian Schmaus
+ * Copyright 2015-2023 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -146,7 +146,7 @@ public final class Configuration {
if (builder.replyTimeout > 0) {
replyTimeout = builder.replyTimeout;
} else {
- replyTimeout = 60000;
+ replyTimeout = 47000;
}
debugger = builder.debugger;
if (StringUtils.isNotEmpty(builder.adminAccountUsername, builder.adminAccountPassword)) {
@@ -488,14 +488,14 @@ public final class Configuration {
}
key = key.substring(SINTTEST.length());
String value = (String) entry.getValue();
- properties.put(key, value);
+ properties.put(key.trim(), value.trim());
}
Builder builder = builder();
builder.setService(properties.getProperty("service"));
builder.setServiceTlsPin(properties.getProperty("serviceTlsPin"));
builder.setSecurityMode(properties.getProperty("securityMode"));
- builder.setReplyTimeout(properties.getProperty("replyTimeout", "60000"));
+ builder.setReplyTimeout(properties.getProperty("replyTimeout", "47000"));
String adminAccountUsername = properties.getProperty("adminAccountUsername");
String adminAccountPassword = properties.getProperty("adminAccountPassword");
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 21d5994b1..8be908fb0 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
@@ -224,7 +224,8 @@ public class SmackIntegrationTestFramework {
}
LOGGER.info("SmackIntegrationTestFramework [" + testRunResult.testRunId
- + "]: Finished scanning for tests, preparing environment");
+ + "]: Finished scanning for tests, preparing environment\n"
+ + "\tJava SE Platform version: " + Runtime.version());
environment = prepareEnvironment();
try {
diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/XmppConnectionManager.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/XmppConnectionManager.java
index e74bced82..0653e5d14 100644
--- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/XmppConnectionManager.java
+++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/XmppConnectionManager.java
@@ -187,14 +187,13 @@ public class XmppConnectionManager {
case inBandRegistration:
accountRegistrationConnection = defaultConnectionDescriptor.construct(sinttestConfiguration);
accountRegistrationConnection.connect();
- accountRegistrationConnection.login(sinttestConfiguration.adminAccountUsername,
- sinttestConfiguration.adminAccountPassword);
if (sinttestConfiguration.accountRegistration == AccountRegistration.inBandRegistration) {
-
adminManager = null;
accountManager = AccountManager.getInstance(accountRegistrationConnection);
} else {
+ accountRegistrationConnection.login(sinttestConfiguration.adminAccountUsername,
+ sinttestConfiguration.adminAccountPassword);
adminManager = ServiceAdministrationManager.getInstanceFor(accountRegistrationConnection);
accountManager = null;
}
@@ -287,7 +286,7 @@ public class XmppConnectionManager {
if (unsuccessfullyDeletedAccountsCount == 0) {
LOGGER.info("Successfully deleted all created accounts ✔");
} else {
- LOGGER.warning("Could not delete all created accounts, " + unsuccessfullyDeletedAccountsCount + " remainaing");
+ LOGGER.warning("Could not delete all created accounts, " + unsuccessfullyDeletedAccountsCount + " remaining");
}
}
@@ -366,11 +365,11 @@ public class XmppConnectionManager {
break;
case inBandRegistration:
if (!accountManager.supportsAccountCreation()) {
- throw new UnsupportedOperationException("Account creation/registation is not supported");
+ throw new UnsupportedOperationException("Account creation/registration is not supported");
}
Set requiredAttributes = accountManager.getAccountAttributes();
if (requiredAttributes.size() > 4) {
- throw new IllegalStateException("Unkown required attributes");
+ throw new IllegalStateException("Unknown required attributes");
}
Map additionalAttributes = new HashMap<>();
additionalAttributes.put("name", "Smack Integration Test");
diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/commands/AdHocCommandIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/commands/AdHocCommandIntegrationTest.java
new file mode 100644
index 000000000..b4821f48e
--- /dev/null
+++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/commands/AdHocCommandIntegrationTest.java
@@ -0,0 +1,353 @@
+/**
+ *
+ * Copyright 2023 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.commands;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.jivesoftware.smack.SmackException.NoResponseException;
+import org.jivesoftware.smack.SmackException.NotConnectedException;
+import org.jivesoftware.smack.XMPPException.XMPPErrorException;
+import org.jivesoftware.smack.packet.StanzaError;
+import org.jivesoftware.smackx.commands.packet.AdHocCommandData;
+import org.jivesoftware.smackx.commands.packet.AdHocCommandDataBuilder;
+import org.jivesoftware.smackx.commands.packet.AdHocCommandDataBuilder.NextStage;
+import org.jivesoftware.smackx.commands.packet.AdHocCommandDataBuilder.PreviousStage;
+import org.jivesoftware.smackx.xdata.FormField;
+import org.jivesoftware.smackx.xdata.form.FillableForm;
+import org.jivesoftware.smackx.xdata.form.SubmitForm;
+import org.jivesoftware.smackx.xdata.packet.DataForm;
+
+import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest;
+import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment;
+import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest;
+
+public class AdHocCommandIntegrationTest extends AbstractSmackIntegrationTest {
+
+ public AdHocCommandIntegrationTest(SmackIntegrationTestEnvironment environment) {
+ super(environment);
+ }
+
+ @SmackIntegrationTest
+ public void singleStageAdHocCommandTest() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
+ AdHocCommandManager manOne = AdHocCommandManager.getInstance(conOne);
+ AdHocCommandManager manTwo = AdHocCommandManager.getInstance(conTwo);
+
+ String commandNode = "test-list";
+ String commandName = "Return a list for testing purposes";
+ AdHocCommandHandlerFactory factory = (String node, String name, String sessionId) -> {
+ return new AdHocCommandHandler.SingleStage(node, name, sessionId) {
+ @Override
+ public AdHocCommandData executeSingleStage(AdHocCommandDataBuilder response) {
+ FormField field = FormField.textPrivateBuilder("my-field").build();
+ DataForm form = DataForm.builder(DataForm.Type.result).addField(field).build();
+
+ response.setForm(form);
+
+ return response.build();
+ }
+ };
+ };
+ manOne.registerCommand(commandNode, commandName, factory);
+ try {
+ AdHocCommand command = manTwo.getRemoteCommand(conOne.getUser(), commandNode);
+
+ AdHocCommandResult result = command.execute();
+ AdHocCommandData response = result.getResponse();
+ DataForm form = response.getForm();
+ FormField field = form.getField("my-field");
+ assertNotNull(field);
+ } finally {
+ manOne.unregisterCommand(commandNode);
+ }
+ }
+
+ private static class MyMultiStageAdHocCommandServer extends AdHocCommandHandler {
+
+ private Integer a;
+ private Integer b;
+
+ private static DataForm createDataForm(String variableName) {
+ FormField field = FormField.textSingleBuilder(variableName).setRequired().build();
+ return DataForm.builder(DataForm.Type.form)
+ .setTitle("Variable " + variableName)
+ .setInstructions("Please provide an integer variable " + variableName)
+ .addField(field)
+ .build();
+ }
+
+ private static DataForm createDataFormOp() {
+ FormField field = FormField.listSingleBuilder("op")
+ .setLabel("Arthimetic Operation")
+ .setRequired()
+ .addOption("+")
+ .addOption("-")
+ .build();
+ return DataForm.builder(DataForm.Type.form)
+ .setTitle("Operation")
+ .setInstructions("Please select the arithmetic operation to be performed with a and b")
+ .addField(field)
+ .build();
+ }
+ private static final DataForm dataFormAskingForA = createDataForm("a");
+ private static final DataForm dataFormAskingForB = createDataForm("b");
+ private static final DataForm dataFormAskingForOp = createDataFormOp();
+
+ MyMultiStageAdHocCommandServer(String node, String name, String sessionId) {
+ super(node, name, sessionId);
+ }
+
+ @Override
+ protected AdHocCommandData execute(AdHocCommandDataBuilder response) throws XMPPErrorException {
+ return response.setForm(dataFormAskingForA).setStatusExecuting(PreviousStage.none,
+ NextStage.nonFinal).build();
+ }
+
+ // TODO: Add API for every case where we return null or throw below.
+ private static Integer extractIntegerField(SubmitForm form, String fieldName) throws XMPPErrorException {
+ FormField field = form.getField(fieldName);
+ if (field == null)
+ throw newBadRequestException("Submitted form does not contain a field of name " + fieldName);
+
+ String fieldValue = field.getFirstValue();
+ if (fieldValue == null)
+ throw newBadRequestException("Submitted form contains field of name " + fieldName + " without value");
+
+ try {
+ return Integer.parseInt(fieldValue);
+ } catch (NumberFormatException e) {
+ throw newBadRequestException("Submitted form contains field of name " + fieldName + " with value " + fieldValue + " that is not an integer");
+ }
+ }
+
+ @Override
+ protected AdHocCommandData next(AdHocCommandDataBuilder response, SubmitForm submittedForm)
+ throws XMPPErrorException {
+ DataForm form;
+ switch (getCurrentStage()) {
+ case 2:
+ a = extractIntegerField(submittedForm, "a");
+ form = dataFormAskingForB;
+ response.setStatusExecuting(PreviousStage.exists, NextStage.nonFinal);
+ break;
+ case 3:
+ b = extractIntegerField(submittedForm, "b");
+ form = dataFormAskingForOp;
+ response.setStatusExecuting(PreviousStage.exists, NextStage.isFinal);
+ break;
+ case 4:
+ // Ad-Hoc Commands particularity: Can get to 'complete' via 'next'.
+ return complete(response, submittedForm);
+ default:
+ throw new IllegalStateException();
+ }
+
+ return response.setForm(form).build();
+ }
+
+ @Override
+ protected AdHocCommandData complete(AdHocCommandDataBuilder response, SubmitForm submittedForm)
+ throws XMPPErrorException {
+ if (getCurrentStage() != 4) {
+ throw new IllegalStateException();
+ }
+
+ if (a == null || b == null) {
+ throw new IllegalStateException();
+ }
+
+ String op = submittedForm.getField("op").getFirstValue();
+
+ int result;
+ switch (op) {
+ case "+":
+ result = a + b;
+ break;
+ case "-":
+ result = a - b;
+ break;
+ default:
+ throw newBadRequestException("Submitted operation " + op + " is neither + nor -");
+ }
+
+ response.setStatusCompleted();
+
+ FormField field = FormField.textSingleBuilder("result").setValue(result).build();
+ DataForm form = DataForm.builder(DataForm.Type.result).setTitle("Result").addField(field).build();
+
+ return response.setForm(form).build();
+ }
+
+ @Override
+ protected AdHocCommandData prev(AdHocCommandDataBuilder response) throws XMPPErrorException {
+ switch (getCurrentStage()) {
+ case 1:
+ return execute(response);
+ case 2:
+ return response.setForm(dataFormAskingForA)
+ .setStatusExecuting(PreviousStage.exists, NextStage.nonFinal)
+ .build();
+ case 3:
+ return response.setForm(dataFormAskingForB)
+ .setStatusExecuting(PreviousStage.exists, NextStage.isFinal)
+ .build();
+ default:
+ throw new IllegalStateException();
+ }
+ }
+
+ @Override
+ public void cancel() {
+ }
+
+ }
+
+ @SmackIntegrationTest
+ public void multiStageAdHocCommandTest() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
+ AdHocCommandManager manOne = AdHocCommandManager.getInstance(conOne);
+ AdHocCommandManager manTwo = AdHocCommandManager.getInstance(conTwo);
+
+ String commandNode = "my-multi-stage-command";
+ String commandName = "An example multi-sage ad-hoc command";
+ AdHocCommandHandlerFactory factory = (String node, String name, String sessionId) -> {
+ return new MyMultiStageAdHocCommandServer(node, name, sessionId);
+ };
+ manOne.registerCommand(commandNode, commandName, factory);
+
+ try {
+ AdHocCommand command = manTwo.getRemoteCommand(conOne.getUser(), commandNode);
+
+ AdHocCommandResult.StatusExecuting result = command.execute().asExecutingOrThrow();
+
+ FillableForm form = result.getFillableForm();
+ form.setAnswer("a", 42);
+
+ SubmitForm submitForm = form.getSubmitForm();
+
+
+ result = command.next(submitForm).asExecutingOrThrow();
+
+ form = result.getFillableForm();
+ form.setAnswer("b", 23);
+
+ submitForm = form.getSubmitForm();
+
+
+ result = command.next(submitForm).asExecutingOrThrow();
+
+ form = result.getFillableForm();
+ form.setAnswer("op", "+");
+
+ submitForm = form.getSubmitForm();
+
+ AdHocCommandResult.StatusCompleted completed = command.complete(submitForm).asCompletedOrThrow();
+
+ String operationResult = completed.getResponse().getForm().getField("result").getFirstValue();
+ assertEquals("65", operationResult);
+ } finally {
+ manTwo.unregisterCommand(commandNode);
+ }
+ }
+
+ @SmackIntegrationTest
+ public void multiStageWithPrevAdHocCommandTest() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
+ AdHocCommandManager manOne = AdHocCommandManager.getInstance(conOne);
+ AdHocCommandManager manTwo = AdHocCommandManager.getInstance(conTwo);
+
+ String commandNode = "my-multi-stage-with-prev-command";
+ String commandName = "An example multi-sage ad-hoc command";
+ AdHocCommandHandlerFactory factory = (String node, String name, String sessionId) -> {
+ return new MyMultiStageAdHocCommandServer(node, name, sessionId);
+ };
+ manOne.registerCommand(commandNode, commandName, factory);
+
+ try {
+ AdHocCommand command = manTwo.getRemoteCommand(conOne.getUser(), commandNode);
+
+ AdHocCommandResult.StatusExecuting result = command.execute().asExecutingOrThrow();
+
+ FillableForm form = result.getFillableForm();
+ form.setAnswer("a", 42);
+
+ SubmitForm submitForm = form.getSubmitForm();
+
+ command.next(submitForm).asExecutingOrThrow();
+
+
+ // Ups, I wanted a different value for 'a', lets execute 'prev' to get back to the previous stage.
+ result = command.prev().asExecutingOrThrow();
+
+ form = result.getFillableForm();
+ form.setAnswer("a", 77);
+
+ submitForm = form.getSubmitForm();
+
+
+ result = command.next(submitForm).asExecutingOrThrow();
+
+ form = result.getFillableForm();
+ form.setAnswer("b", 23);
+
+ submitForm = form.getSubmitForm();
+
+
+ result = command.next(submitForm).asExecutingOrThrow();
+
+ form = result.getFillableForm();
+ form.setAnswer("op", "+");
+
+ submitForm = form.getSubmitForm();
+
+ AdHocCommandResult.StatusCompleted completed = command.complete(submitForm).asCompletedOrThrow();
+
+ String operationResult = completed.getResponse().getForm().getField("result").getFirstValue();
+ assertEquals("100", operationResult);
+ } finally {
+ manTwo.unregisterCommand(commandNode);
+ }
+ }
+
+ @SmackIntegrationTest
+ public void multiStageInvalidArgAdHocCommandTest() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
+ AdHocCommandManager manOne = AdHocCommandManager.getInstance(conOne);
+ AdHocCommandManager manTwo = AdHocCommandManager.getInstance(conTwo);
+
+ String commandNode = "my-multi-stage-invalid-arg-command";
+ String commandName = "An example multi-sage ad-hoc command";
+ AdHocCommandHandlerFactory factory = (String node, String name, String sessionId) -> {
+ return new MyMultiStageAdHocCommandServer(node, name, sessionId);
+ };
+ manOne.registerCommand(commandNode, commandName, factory);
+
+ try {
+ AdHocCommand command = manTwo.getRemoteCommand(conOne.getUser(), commandNode);
+
+ AdHocCommandResult.StatusExecuting result = command.execute().asExecutingOrThrow();
+
+ FillableForm form = result.getFillableForm();
+ form.setAnswer("a", "forty-two");
+
+ SubmitForm submitForm = form.getSubmitForm();
+
+ XMPPErrorException exception = assertThrows(XMPPErrorException.class, () -> command.next(submitForm));
+ assertEquals(exception.getStanzaError().getCondition(), StanzaError.Condition.bad_request);
+ } finally {
+ manTwo.unregisterCommand(commandNode);
+ }
+ }
+}
diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/commands/package-info.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/commands/package-info.java
new file mode 100644
index 000000000..22bfe9d75
--- /dev/null
+++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/commands/package-info.java
@@ -0,0 +1,22 @@
+/**
+ *
+ * Copyright 2023 Florian Schmaus
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+/**
+ * Smacks implementation of XEP-0050: Ad-Hoc Commands.
+ */
+package org.jivesoftware.smackx.commands;
diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/AbstractMultiUserChatIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/AbstractMultiUserChatIntegrationTest.java
index 5b2879c63..22316ed6e 100644
--- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/AbstractMultiUserChatIntegrationTest.java
+++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/AbstractMultiUserChatIntegrationTest.java
@@ -1,6 +1,6 @@
/**
*
- * Copyright 2021 Florian Schmaus
+ * Copyright 2021-2023 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.jid.parts.Resourcepart;
import org.jxmpp.stringprep.XmppStringprepException;
-public class AbstractMultiUserChatIntegrationTest extends AbstractSmackIntegrationTest {
+public abstract class AbstractMultiUserChatIntegrationTest extends AbstractSmackIntegrationTest {
final String randomString = StringUtils.insecureRandomString(6);
diff --git a/smack-java8-full/src/main/java/org/jivesoftware/smack/full/WebSocketConnectionTest.java b/smack-java8-full/src/main/java/org/jivesoftware/smack/full/WebSocketConnectionTest.java
deleted file mode 100644
index 54c3d30d5..000000000
--- a/smack-java8-full/src/main/java/org/jivesoftware/smack/full/WebSocketConnectionTest.java
+++ /dev/null
@@ -1,110 +0,0 @@
-/**
- *
- * Copyright 2021 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.full;
-
-import java.io.IOException;
-import java.net.URISyntaxException;
-import java.util.Date;
-import java.util.logging.Logger;
-
-import org.jivesoftware.smack.SmackConfiguration;
-import org.jivesoftware.smack.SmackException;
-import org.jivesoftware.smack.XMPPException;
-
-import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection;
-import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionConfiguration;
-import org.jivesoftware.smack.debugger.ConsoleDebugger;
-import org.jivesoftware.smack.packet.Message;
-import org.jivesoftware.smack.websocket.XmppWebSocketTransportModuleDescriptor;
-import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
-
-import org.jxmpp.util.XmppDateTime;
-
-public class WebSocketConnectionTest {
-
- static {
- SmackConfiguration.DEBUG = true;
- }
-
- public static void main(String[] args)
- throws URISyntaxException, SmackException, IOException, XMPPException, InterruptedException {
- String jid, password, websocketEndpoint, messageTo = null;
- if (args.length < 3 || args.length > 4) {
- throw new IllegalArgumentException();
- }
-
- jid = args[0];
- password = args[1];
- websocketEndpoint = args[2];
- if (args.length >= 4) {
- messageTo = args[3];
- }
-
- testWebSocketConnection(jid, password, websocketEndpoint, messageTo);
- }
-
- public static void testWebSocketConnection(String jid, String password, String websocketEndpoint)
- throws URISyntaxException, SmackException, IOException, XMPPException, InterruptedException {
- testWebSocketConnection(jid, password, websocketEndpoint, null);
- }
-
- public static void testWebSocketConnection(String jid, String password, String websocketEndpoint, String messageTo)
- throws URISyntaxException, SmackException, IOException, XMPPException, InterruptedException {
- ModularXmppClientToServerConnectionConfiguration.Builder builder = ModularXmppClientToServerConnectionConfiguration.builder();
- builder.removeAllModules()
- .setXmppAddressAndPassword(jid, password)
- .setDebuggerFactory(ConsoleDebugger.Factory.INSTANCE)
- ;
-
- XmppWebSocketTransportModuleDescriptor.Builder websocketBuilder = XmppWebSocketTransportModuleDescriptor.getBuilder(builder);
- websocketBuilder.explicitlySetWebSocketEndpointAndDiscovery(websocketEndpoint, false);
- builder.addModule(websocketBuilder.build());
-
- ModularXmppClientToServerConnectionConfiguration config = builder.build();
- ModularXmppClientToServerConnection connection = new ModularXmppClientToServerConnection(config);
-
- connection.setReplyTimeout(5 * 60 * 1000);
-
- connection.addConnectionStateMachineListener((event, c) -> {
- Logger.getAnonymousLogger().info("Connection event: " + event);
- });
-
- connection.connect();
-
- connection.login();
-
- if (messageTo != null) {
- Message message = connection.getStanzaFactory().buildMessageStanza()
- .to(messageTo)
- .setBody("It is alive! " + XmppDateTime.formatXEP0082Date(new Date()))
- .build()
- ;
- connection.sendStanza(message);
- }
-
- Thread.sleep(1000);
-
- connection.disconnect();
-
- ModularXmppClientToServerConnection.Stats connectionStats = connection.getStats();
- ServiceDiscoveryManager.Stats serviceDiscoveryManagerStats = ServiceDiscoveryManager.getInstanceFor(connection).getStats();
-
- // CHECKSTYLE:OFF
- System.out.println("WebSocket successfully finished, yeah!\n" + connectionStats + '\n' + serviceDiscoveryManagerStats);
- // CHECKSTYLE:ON
- }
-}
diff --git a/smack-java8-full/src/main/java/org/jivesoftware/smackx/package-info.java b/smack-java8-full/src/main/java/org/jivesoftware/smackx/package-info.java
index 8b3550028..803afc7da 100644
--- a/smack-java8-full/src/main/java/org/jivesoftware/smackx/package-info.java
+++ b/smack-java8-full/src/main/java/org/jivesoftware/smackx/package-info.java
@@ -584,12 +584,6 @@
* Multi-User Chats for mobile XMPP applications and specific environment. |
*
*
- * Group Chat Invitations |
- * |
- * |
- * Send invitations to other users to join a group chat room. |
- *
- *
* Jive Properties |
* |
* |
diff --git a/smack-omemo-signal-integration-test/src/main/java/org/igniterealtime/smack/inttest/smack_omemo_signal/SmackOmemoSignalIntegrationTestFramework.java b/smack-omemo-signal-integration-test/src/main/java/org/igniterealtime/smack/inttest/smack_omemo_signal/SmackOmemoSignalIntegrationTestFramework.java
index aa17e3593..d79154e4a 100644
--- a/smack-omemo-signal-integration-test/src/main/java/org/igniterealtime/smack/inttest/smack_omemo_signal/SmackOmemoSignalIntegrationTestFramework.java
+++ b/smack-omemo-signal-integration-test/src/main/java/org/igniterealtime/smack/inttest/smack_omemo_signal/SmackOmemoSignalIntegrationTestFramework.java
@@ -27,6 +27,7 @@ import java.security.InvalidKeyException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
+import java.security.Security;
import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
@@ -38,10 +39,15 @@ import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException;
import org.jivesoftware.smackx.omemo.signal.SignalOmemoService;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.igniterealtime.smack.inttest.SmackIntegrationTestFramework;
public class SmackOmemoSignalIntegrationTestFramework {
+ static {
+ Security.addProvider(new BouncyCastleProvider());
+ }
+
public static void main(String[] args) throws InvalidKeyException, NoSuchPaddingException,
InvalidAlgorithmParameterException, IllegalBlockSizeException,
BadPaddingException, NoSuchAlgorithmException, NoSuchProviderException, SmackException,
diff --git a/smack-omemo-signal/build.gradle b/smack-omemo-signal/build.gradle
index c55aa3033..f29374f10 100644
--- a/smack-omemo-signal/build.gradle
+++ b/smack-omemo-signal/build.gradle
@@ -6,7 +6,7 @@ dependencies {
api project(":smack-im")
api project(":smack-extensions")
api project(":smack-omemo")
- implementation 'org.whispersystems:signal-protocol-java:2.6.2'
+ implementation 'org.whispersystems:signal-protocol-java:2.8.1'
testFixturesApi(testFixtures(project(":smack-core")))
testImplementation project(path: ":smack-omemo", configuration: "testRuntime")
diff --git a/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoStoreConnector.java b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoStoreConnector.java
index 69b43927c..ccec0ad5e 100644
--- a/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoStoreConnector.java
+++ b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoStoreConnector.java
@@ -122,6 +122,22 @@ public class SignalOmemoStoreConnector
return true;
}
+ @Override
+ public IdentityKey getIdentity(SignalProtocolAddress address) {
+ OmemoDevice device;
+ try {
+ device = asOmemoDevice(address);
+ } catch (XmppStringprepException e) {
+ throw new AssertionError(e);
+ }
+
+ try {
+ return omemoStore.loadOmemoIdentityKey(getOurDevice(), device);
+ } catch (IOException | CorruptedOmemoKeyException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
@Override
public PreKeyRecord loadPreKey(int i) throws InvalidKeyIdException {
PreKeyRecord preKey;
diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoAesCipher.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoAesCipher.java
index aee330764..4edb10c4d 100644
--- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoAesCipher.java
+++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/OmemoAesCipher.java
@@ -1,6 +1,6 @@
/**
*
- * Copyright 2017 Paul Schaub, 2019-2021 Florian Schmaus
+ * Copyright 2017 Paul Schaub, 2019-2023 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -47,7 +47,7 @@ public class OmemoAesCipher {
String message = "Unable to perform " + OmemoConstants.Crypto.CIPHERMODE
+ " operation requires by OMEMO. Ensure that a suitable crypto provider for is available."
+ " For example Bouncycastle on Android (BouncyCastleProvider)";
- throw new AssertionError(message);
+ throw new AssertionError(message, e);
}
}
diff --git a/smack-openpgp/build.gradle b/smack-openpgp/build.gradle
index cde598465..65484bde3 100644
--- a/smack-openpgp/build.gradle
+++ b/smack-openpgp/build.gradle
@@ -8,7 +8,7 @@ dependencies {
api project(':smack-extensions')
api project(':smack-experimental')
- api 'org.pgpainless:pgpainless-core:1.3.1'
+ api 'org.pgpainless:pgpainless-core:1.5.3'
testImplementation "org.bouncycastle:bcprov-jdk18on:${bouncyCastleVersion}"
diff --git a/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/crypto/PainlessOpenPgpProvider.java b/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/crypto/PainlessOpenPgpProvider.java
index 2fa3bbe77..304e3e006 100644
--- a/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/crypto/PainlessOpenPgpProvider.java
+++ b/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/crypto/PainlessOpenPgpProvider.java
@@ -220,7 +220,7 @@ public class PainlessOpenPgpProvider implements OpenPgpProvider {
cipherStream.close();
plainText.close();
- OpenPgpMetadata info = cipherStream.getResult();
+ OpenPgpMetadata info = cipherStream.getMetadata().toLegacyMetadata();
OpenPgpMessage.State state;
if (info.isSigned()) {
diff --git a/smack-repl/build.gradle b/smack-repl/build.gradle
index 70e055ab7..6e098eb52 100644
--- a/smack-repl/build.gradle
+++ b/smack-repl/build.gradle
@@ -1,5 +1,5 @@
plugins {
- id "com.github.alisiikh.scalastyle_2.12" version "2.0.2"
+ id "com.github.alisiikh.scalastyle_2.12" version "2.1.0"
}
description = """\
@@ -9,18 +9,14 @@ apply plugin: 'scala'
apply plugin: 'com.github.alisiikh.scalastyle_2.12'
ext {
- scalaVersion = '2.13.6'
+ scalaVersion = '2.13.12'
}
dependencies {
- // Smack's integration test framework (sintest) depends on
- // smack-java*-full and since we may want to use parts of sinttest
- // in the REPL, we simply depend sinttest.
- api project(':smack-integration-test')
- api project(':smack-omemo-signal')
+ api project(':smack-examples')
implementation "org.scala-lang:scala-library:$scalaVersion"
- implementation "com.lihaoyi:ammonite_$scalaVersion:2.4.0"
+ implementation "com.lihaoyi:ammonite_$scalaVersion:2.5.11"
}
scalaStyle {
diff --git a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/impl/AbstractWebSocket.java b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/impl/AbstractWebSocket.java
index bbe4ac749..6924cc6bd 100644
--- a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/impl/AbstractWebSocket.java
+++ b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/impl/AbstractWebSocket.java
@@ -16,17 +16,25 @@
*/
package org.jivesoftware.smack.websocket.impl;
+import java.io.IOException;
+import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.ssl.SSLSession;
+import javax.xml.namespace.QName;
import org.jivesoftware.smack.SmackFuture;
import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal;
import org.jivesoftware.smack.debugger.SmackDebugger;
import org.jivesoftware.smack.packet.TopLevelStreamElement;
import org.jivesoftware.smack.packet.XmlEnvironment;
+import org.jivesoftware.smack.util.PacketParserUtils;
import org.jivesoftware.smack.websocket.WebSocketException;
+import org.jivesoftware.smack.websocket.elements.WebSocketCloseElement;
+import org.jivesoftware.smack.websocket.elements.WebSocketOpenElement;
import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpoint;
+import org.jivesoftware.smack.xml.XmlPullParser;
+import org.jivesoftware.smack.xml.XmlPullParserException;
public abstract class AbstractWebSocket {
@@ -95,28 +103,34 @@ public abstract class AbstractWebSocket {
static String getStreamFromOpenElement(String openElement) {
String streamElement = openElement.replaceFirst("\\A\\s*\\z", " xmlns:stream='http://etherx.jabber.org/streams'>");
+ .replaceFirst("/>\\s*\\z", " xmlns:stream='http://etherx.jabber.org/streams'>")
+ .replaceFirst(">\\s*\\z", " xmlns:stream='http://etherx.jabber.org/streams'>");
+
return streamElement;
}
- // TODO: Make this method less fragile, e.g. by parsing a little bit into the element to ensure that this is an
- // element qualified by the correct namespace.
static boolean isOpenElement(String text) {
- if (text.startsWith(" element qualified by the correct namespace. The fragility comes due the fact that the element could,
- // inter alia, be specified as
- //
static boolean isCloseElement(String text) {
- if (text.startsWith("")) {
- return true;
+ XmlPullParser parser;
+ try {
+ parser = PacketParserUtils.getParserFor(text);
+ QName qname = parser.getQName();
+ return qname.equals(WebSocketCloseElement.QNAME);
+ } catch (XmlPullParserException | IOException e) {
+ LOGGER.log(Level.WARNING, "Could not inspect \"" + text + "\" for close element", e);
+ return false;
}
- return false;
}
protected void onWebSocketFailure(Throwable throwable) {
diff --git a/smack-websocket/src/test/java/org/jivesoftware/smack/websocket/impl/AbstractWebSocketTest.java b/smack-websocket/src/test/java/org/jivesoftware/smack/websocket/impl/AbstractWebSocketTest.java
index 23f274616..5280efe7b 100644
--- a/smack-websocket/src/test/java/org/jivesoftware/smack/websocket/impl/AbstractWebSocketTest.java
+++ b/smack-websocket/src/test/java/org/jivesoftware/smack/websocket/impl/AbstractWebSocketTest.java
@@ -24,18 +24,20 @@ import org.junit.jupiter.api.Test;
public final class AbstractWebSocketTest {
private static final String OPEN_ELEMENT = "";
+ private static final String OPEN_ELEMENT_EXPANDED = "";
private static final String OPEN_STREAM = "";
private static final String CLOSE_ELEMENT = "";
@Test
public void getStreamFromOpenElementTest() {
- String generatedOpenStream = AbstractWebSocket.getStreamFromOpenElement(OPEN_ELEMENT);
- assertEquals(generatedOpenStream, OPEN_STREAM);
+ assertEquals(OPEN_STREAM, AbstractWebSocket.getStreamFromOpenElement(OPEN_ELEMENT));
+ assertEquals(OPEN_STREAM, AbstractWebSocket.getStreamFromOpenElement(OPEN_ELEMENT_EXPANDED));
}
@Test
public void isOpenElementTest() {
assertTrue(AbstractWebSocket.isOpenElement(OPEN_ELEMENT));
+ assertTrue(AbstractWebSocket.isOpenElement(OPEN_ELEMENT_EXPANDED));
assertFalse(AbstractWebSocket.isOpenElement(OPEN_STREAM));
}
diff --git a/version b/version
index 891d39303..0c77728e0 100644
--- a/version
+++ b/version
@@ -1 +1 @@
-4.5.0-alpha2-SNAPSHOT
+4.5.0-alpha3-SNAPSHOT