diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2fb93a48c..fa9bb447a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,13 +6,14 @@ jobs: build: name: Build Smack - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: java: - - 11 + - 17 + - 21 env: - PRIMARY_JAVA_VERSION: 11 + PRIMARY_JAVA_VERSION: 21 steps: - name: Checkout @@ -52,10 +53,10 @@ jobs: - name: Install GraphViz run: sudo apt update && sudo apt install graphviz - name: Install Android SDK Manager - uses: android-actions/setup-android@v2 + uses: android-actions/setup-android@v3 - name: Install Android SDK run: | - sdkmanager "platforms;android-19" + sdkmanager "platforms;android-23" # Testing - name: Gradle Check @@ -71,15 +72,22 @@ jobs: run: ./gradlew javadocAll --stacktrace # Test Coverage Report - - name: Jacoco Test Coverage + - name: Aggregated Jacoco Test Coverage Report if: ${{ matrix.java == env.PRIMARY_JAVA_VERSION }} - run: ./gradlew jacocoRootReport coveralls - env: - COVERALLS_REPO_TOKEN: S2ecSJja2cKJa9yv45C8ZFPohXuRrTXKd + run: | + ./gradlew smack-java11-full:testCodeCoverageReport + + # Coveralls + - name: Report coverage stats to Coveralls + if: ${{ matrix.java == env.PRIMARY_JAVA_VERSION }} + uses: coverallsapp/github-action@v2 + with: + format: jacoco + file: smack-java11-full/build/reports/jacoco/testCodeCoverageReport/testCodeCoverageReport.xml # Upload build artifacts - name: Upload build artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: smack-java-${{ matrix.java }} path: | diff --git a/.gitignore b/.gitignore index 2739029bb..ec8037f60 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ *.iml *.ipr *.iws +!.idea/icon.svg # Mac OS X .DS_Store @@ -11,7 +12,6 @@ .project .settings .gradle -gradle.properties build/ core/build/ diff --git a/.idea/icon.svg b/.idea/icon.svg new file mode 100644 index 000000000..1d9b3fb6f --- /dev/null +++ b/.idea/icon.svg @@ -0,0 +1,89 @@ + +image/svg+xml \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 93592bbc9..b413e7f75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ # Smack Changelog +# 4.4.8 -- 2024-04-02 + +### Improvement + +[SMACK-941](https://igniterealtime.atlassian.net/browse/SMACK-941) Suppress "roster not loaded while processing presence" warning if its caused by the reflected self-presence + +### Bug + +[SMACK-938](https://igniterealtime.atlassian.net/browse/SMACK-938) Busy loop in SmackReactor + +[SMACK-940](https://igniterealtime.atlassian.net/browse/SMACK-940) Ignore IPv6 Zone IDs in incoming streamhost candidates + +# 4.4.7 -- 2023-11-25 + +### Improvement + +- [SMACK-929](https://igniterealtime.atlassian.net/browse/SMACK-929) Ignore IPv6 Zone IDs in incoming Jingle candidates +- [SMACK-934](https://igniterealtime.atlassian.net/browse/SMACK-934) Deprecate and remove ChatMarkersManager.isSupportedByServer\(\) +- [SMACK-937](https://igniterealtime.atlassian.net/browse/SMACK-937) Avoid unnecessary feature lookups by making the EntityCaps listener synchronous + +### Bug + +- [SMACK-927](https://igniterealtime.atlassian.net/browse/SMACK-927) Deadlock due to recveive listeners may be invoked after AbstractXMPPConnection.invokeStanzaCollectorsAndNotifyRecvListeners\(\) returned +- [SMACK-930](https://igniterealtime.atlassian.net/browse/SMACK-930) Rename ELEMENT 'candidate-activated' to 'activated' per XEP-0260 +- [SMACK-931](https://igniterealtime.atlassian.net/browse/SMACK-931) IQ error stanza generation does not allow adding of an extension element + # 4.4.6 -- 2022-06-29 ### Bug diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..4689ef8af --- /dev/null +++ b/Makefile @@ -0,0 +1,30 @@ +GRADLE ?= ./gradlew + +.PHONY: all +all: check jacocoRootReport javadocAll sinttest + +.PHONY: codecov +codecov: + $(GRADLE) smack-java11-full:testCodeCoverageReport + echo "Report available at smack-java11-full/build/reports/jacoco/testCodeCoverageReport/html/index.html" + +.PHONY: check +check: + $(GRADLE) $@ + +.PHONY: eclipse +eclipse: + $(GRADLE) $@ + +.PHONY: sinttest +sinttest: + $(GRADLE) $@ + +.PHONY: jacocoRootReport +jacocoRootReport: + $(GRADLE) $@ + +.PHONY: javadocAll +javadocAll: + $(GRADLE) $@ + echo "Smack javadoc available at build/javadoc/index.html" diff --git a/build-logic/build.gradle b/build-logic/build.gradle new file mode 100644 index 000000000..7b02d03a7 --- /dev/null +++ b/build-logic/build.gradle @@ -0,0 +1,14 @@ +plugins { + id 'groovy-gradle-plugin' +} + +repositories { + gradlePluginPortal() +} + +dependencies { + implementation "biz.aQute.bnd:biz.aQute.bnd.gradle:7.0.0" + implementation "me.champeau.jmh:jmh-gradle-plugin:0.7.2" + implementation "net.ltgt.gradle:gradle-errorprone-plugin:4.0.1" + implementation "ru.vyarus:gradle-animalsniffer-plugin:1.7.1" +} diff --git a/build-logic/settings.gradle b/build-logic/settings.gradle new file mode 100644 index 000000000..d082ce7e0 --- /dev/null +++ b/build-logic/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'smack-build-logic' diff --git a/build-logic/src/main/groovy/org.igniterealtime.smack.android-boot-classpath-conventions.gradle b/build-logic/src/main/groovy/org.igniterealtime.smack.android-boot-classpath-conventions.gradle new file mode 100644 index 000000000..c14cb3199 --- /dev/null +++ b/build-logic/src/main/groovy/org.igniterealtime.smack.android-boot-classpath-conventions.gradle @@ -0,0 +1,6 @@ +compileJava { + options.bootstrapClasspath = files(androidBootClasspath) +} +javadoc { + classpath += files(androidBootClasspath) +} diff --git a/build-logic/src/main/groovy/org.igniterealtime.smack.android-conventions.gradle b/build-logic/src/main/groovy/org.igniterealtime.smack.android-conventions.gradle new file mode 100644 index 000000000..94beb432c --- /dev/null +++ b/build-logic/src/main/groovy/org.igniterealtime.smack.android-conventions.gradle @@ -0,0 +1,10 @@ +plugins { + id 'ru.vyarus.animalsniffer' + id 'org.igniterealtime.smack.global-conventions' +} +dependencies { + signature "net.sf.androidscents.signature:android-api-level-${smackMinAndroidSdk}:6.0_r3@signature" +} +animalsniffer { + sourceSets = [sourceSets.main] +} diff --git a/build-logic/src/main/groovy/org.igniterealtime.smack.application-conventions.gradle b/build-logic/src/main/groovy/org.igniterealtime.smack.application-conventions.gradle new file mode 100644 index 000000000..fa4c7011e --- /dev/null +++ b/build-logic/src/main/groovy/org.igniterealtime.smack.application-conventions.gradle @@ -0,0 +1,12 @@ +plugins { + id 'application' +} + +application { + applicationDefaultJvmArgs = ["-enableassertions"] +} + +run { + // Pass all system properties down to the "application" run + systemProperties System.getProperties() +} diff --git a/build-logic/src/main/groovy/org.igniterealtime.smack.global-conventions.gradle b/build-logic/src/main/groovy/org.igniterealtime.smack.global-conventions.gradle new file mode 100644 index 000000000..5bd92c7ee --- /dev/null +++ b/build-logic/src/main/groovy/org.igniterealtime.smack.global-conventions.gradle @@ -0,0 +1,37 @@ +ext { + javaVersion = JavaVersion.VERSION_11 + javaMajor = javaVersion.getMajorVersion() + smackMinAndroidSdk = 23 + + androidBootClasspath = { getAndroidRuntimeJar() } +} + +repositories { + mavenLocal() + mavenCentral() +} + +def getAndroidRuntimeJar() { + def androidApiLevel = ext.smackMinAndroidSdk + def androidHome = getAndroidHome() + def androidJar = new File("$androidHome/platforms/android-${androidApiLevel}/android.jar") + if (androidJar.isFile()) { + return androidJar + } else { + throw new Exception("Can't find android.jar for API level ${androidApiLevel}. Please install corresponding SDK platform package") + } +} +def getAndroidJavadocOffline() { + def androidHome = getAndroidHome() + return androidHome.toString() + "/docs/reference" +} + +def getAndroidHome() { + def androidHomeEnv = System.getenv("ANDROID_HOME") + if (androidHomeEnv == null) { + throw new Exception("ANDROID_HOME environment variable is not set") + } + def androidHome = new File(androidHomeEnv) + if (!androidHome.isDirectory()) throw new Exception("Environment variable ANDROID_HOME is not pointing to a directory") + return androidHome +} diff --git a/build-logic/src/main/groovy/org.igniterealtime.smack.java-common-conventions.gradle b/build-logic/src/main/groovy/org.igniterealtime.smack.java-common-conventions.gradle new file mode 100644 index 000000000..598fdfa67 --- /dev/null +++ b/build-logic/src/main/groovy/org.igniterealtime.smack.java-common-conventions.gradle @@ -0,0 +1,367 @@ +plugins { + id 'biz.aQute.bnd.builder' + id 'checkstyle' + id 'eclipse' + id 'idea' + id 'jacoco' + id 'java' + id 'java-library' + id 'java-test-fixtures' + id 'maven-publish' + id 'net.ltgt.errorprone' + id 'signing' + + id 'jacoco-report-aggregation' + id 'test-report-aggregation' + + id 'org.igniterealtime.smack.global-conventions' + id 'org.igniterealtime.smack.javadoc-conventions' +} + +version readVersionFile() + +ext { + isSnapshot = version.endsWith('-SNAPSHOT') + gitCommit = getGitCommit() + rootConfigDir = new File(rootDir, 'config') + sonatypeCredentialsAvailable = project.hasProperty('sonatypeUsername') && project.hasProperty('sonatypePassword') + isReleaseVersion = !isSnapshot + isContinuousIntegrationEnvironment = Boolean.parseBoolean(System.getenv('CI')) + signingRequired = !(isSnapshot || isContinuousIntegrationEnvironment) + sonatypeSnapshotUrl = 'https://oss.sonatype.org/content/repositories/snapshots' + sonatypeStagingUrl = 'https://oss.sonatype.org/service/local/staging/deploy/maven2' + builtDate = (new java.text.SimpleDateFormat("yyyy-MM-dd")).format(new Date()) + oneLineDesc = 'An Open Source XMPP (Jabber) client library' + + jxmppVersion = '[1.1.0-beta1, 1.1.999]' + miniDnsVersion = '[1.1.0-alpha3, 1.1.999]' + junitVersion = '5.9.2' + commonsIoVersion = '2.6' + bouncyCastleVersion = '1.73' + guavaVersion = '30.1-jre' + mockitoVersion = '5.13.0' + orgReflectionsVersion = '0.9.11' + + if (project.hasProperty("useSonatype")) { + useSonatype = project.getProperty("useSonatype").toBoolean() + } else { + // Default to true + useSonatype = true + } + + gplLicensedProjects = [ + ':smack-examples', + ':smack-omemo-signal', + ':smack-omemo-signal-integration-test', + ':smack-repl' + ].collect{ project(it) } +} + +group = 'org.igniterealtime.smack' + +java { + sourceCompatibility = javaVersion + targetCompatibility = sourceCompatibility +} + +eclipse { + classpath { + downloadJavadoc = true + } +} + +// Make all project's 'test' target depend on javadoc, so that +// javadoc is also linted. +test.dependsOn javadoc + +tasks.withType(JavaCompile) { + // Some systems may not have set their platform default + // converter to 'utf8', but we use unicode in our source + // files. Therefore ensure that javac uses unicode + options.encoding = "utf8" + options.compilerArgs = [ + '-Xlint:all', + // Set '-options' because a non-java7 javac will emit a + // warning if source/target is set to 1.7 and + // bootclasspath is *not* set. + '-Xlint:-options', + // TODO: Enable xlint serial + '-Xlint:-serial', + '-Werror', + ] +} +if (JavaVersion.current().isJava8Compatible()) { + tasks.withType(Javadoc) { + // The '-quiet' as second argument is actually a hack, + // since the one parameter addStringOption doesn't seem to + // work, we extra add '-quiet', which is added anyway by + // gradle. + // We disable 'missing' as we do most of javadoc checking via checkstyle. + options.addStringOption('Xdoclint:all,-missing', '-quiet') + // Abort on javadoc warnings. + // See JDK-8200363 (https://bugs.openjdk.java.net/browse/JDK-8200363) + // for information about the -Xwerror option. + options.addStringOption('Xwerror', '-quiet') + } +} + +if (JavaVersion.current().isJava9Compatible()) { + tasks.withType(JavaCompile) { + options.compilerArgs.addAll([ + '--release', javaMajor, + ]) + } +} + +jacoco { + toolVersion = "0.8.12" +} + +jacocoTestReport { + dependsOn test + reports { + xml.required = true + } +} + +dependencies { + testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" + + testFixturesApi "org.junit.jupiter:junit-jupiter-api:$junitVersion" + testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" + // https://stackoverflow.com/a/77274251/194894 + testRuntimeOnly "org.junit.platform:junit-platform-launcher:1.11.0" + + // The smack-extensions subproject uses mockito in its fest + // fixtures, and we want to have mockito also available in + // test, so we use API here. + testFixturesApi "org.mockito:mockito-core:${mockitoVersion}" + + testImplementation 'com.jamesmurty.utils:java-xmlbuilder:1.2' + + errorprone 'com.google.errorprone:error_prone_core:2.32.0' +} + +test { + useJUnitPlatform() + + maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1 + + // Enable full stacktraces of failed tests. Especially handy + // for CI environments. + testLogging { + events "failed" + exceptionFormat "full" + } +} + +jar { + manifest { + attributes( + 'Implementation-Version': version, + 'Implementation-GitRevision': gitCommit, + 'Built-JDK': System.getProperty('java.version'), + 'Built-Gradle': gradle.gradleVersion, + 'Built-By': System.getProperty('user.name') + ) + } + + bundle { + bnd( + '-removeheaders': 'Tool, Bnd-*', + '-exportcontents': '*', + ) + } +} + +checkstyle { + toolVersion = '8.27' + + if (project in gplLicensedProjects) { + configProperties.checkstyleLicenseHeader = "${project.name}-gplv3-license-header" + } else { + configProperties.checkstyleLicenseHeader = "header" + } +} +task sourcesJar(type: Jar, dependsOn: classes) { + archiveClassifier = 'sources' + from sourceSets.main.allSource +} +task javadocJar(type: Jar, dependsOn: javadoc) { + archiveClassifier = 'javadoc' + from javadoc.destinationDir +} +task testsJar(type: Jar) { + archiveClassifier = 'tests' + from sourceSets.test.output +} +configurations { + testRuntime +} +artifacts { + // Add a 'testRuntime' configuration including the tests so that + // it can be consumed by other projects (smack-omemo-signal for + // example). See http://stackoverflow.com/a/21946676/194894 + testRuntime testsJar +} + +publishing { + publications { + mavenJava(MavenPublication) { + from components.java + artifact sourcesJar + artifact javadocJar + artifact testsJar + pom { + name = 'Smack' + packaging = 'jar' + inceptionYear = '2003' + url = 'http://www.igniterealtime.org/projects/jxmpp/' + afterEvaluate { + description = project.description + } + + issueManagement { + system = 'JIRA' + url = 'http://issues.igniterealtime.org/browse/SMACK' + } + + scm { + url = 'https://github.com/igniterealtime/Smack' + connection = 'scm:git:https://github.com/igniterealtime/Smack.git' + developerConnection = 'scm:git:https://github.com/igniterealtime/Smack.git' + } + + licenses { + if (project in gplLicensedProjects) { + license { + name = 'GNU General Public License, version 3 or any later version' + url = 'https://www.gnu.org/licenses/gpl.txt' + distribution = 'repo' + } + } else { + license { + name = 'The Apache Software License, Version 2.0' + url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' + distribution = 'repo' + } + } + } + + developers { + developer { + id = 'flow' + name = 'Florian Schmaus' + email = 'flow@igniterealtime.org' + } + } + } + } + } + repositories { + if (sonatypeCredentialsAvailable && useSonatype) { + maven { + url isSnapshot ? sonatypeSnapshotUrl : sonatypeStagingUrl + credentials { + username = sonatypeUsername + password = sonatypePassword + } + } + } + // Use + // gradle publish -P customRepoUrl=https://www.igniterealtime.org/archiva/repository/maven -P customRepoUsername=bamboo -P customRepoPassword=hidden -P useSonatype=false + // to deploy to this repo. + if (project.hasProperty("customRepoUrl")) { + maven { + name 'customRepo' + url customRepoUrl + if (project.hasProperty("customRepoUsername")) { + credentials { + username customRepoUsername + password customRepoPassword + } + } + } + } + } +} + +// Workaround for gpg signatory not supporting the 'required' option +// See https://github.com/gradle/gradle/issues/5064#issuecomment-381924984 +// Note what we use 'signing.gnupg.keyName' instead of 'signing.keyId'. +tasks.withType(Sign) { + onlyIf { + project.hasProperty('signing.gnupg.keyName') + } +} +signing { + required { signingRequired } + useGpgCmd() + sign publishing.publications.mavenJava +} + +tasks.withType(JavaCompile) { + options.errorprone { + disableWarningsInGeneratedCode = true + excludedPaths = ".*/jmh_generated/.*" + error( + "UnusedVariable", + "UnusedMethod", + "MethodCanBeStatic", + ) + errorproneArgs = [ + // Disable MissingCasesInEnumSwitch error prone check + // because this check is already done by javac as incomplete-switch. + '-Xep:MissingCasesInEnumSwitch:OFF', + '-Xep:StringSplitter:OFF', + '-Xep:JavaTimeDefaultTimeZone:OFF', + '-Xep:InlineMeSuggester:OFF', + ] + } +} + +// Work around https://github.com/gradle/gradle/issues/4046 +task copyJavadocDocFiles(type: Copy) { + from('src/javadoc') + into 'build/docs/javadoc' + include '**/doc-files/*.*' +} +javadoc.dependsOn copyJavadocDocFiles + +def getGitCommit() { + def projectDirFile = new File("$projectDir") + + def cmd = 'git describe --always --tags --dirty=+' + def proc = cmd.execute(null, projectDirFile) + + def exitStatus = proc.waitFor() + if (exitStatus != 0) return "non-git build" + + def gitCommit = proc.text.trim() + assert !gitCommit.isEmpty() + gitCommit +} + +def getAndroidRuntimeJar() { + def androidHome = new File("$System.env.ANDROID_HOME") + if (!androidHome.isDirectory()) throw new Exception("ANDROID_HOME not found or set") + def androidJar = new File("$androidHome/platforms/android-$smackMinAndroidSdk/android.jar") + if (androidJar.isFile()) { + return androidJar + } else { + throw new Exception("Can't find android.jar for $smackMinAndroidSdk API. Please install corresponding SDK platform package") + } +} + +def readVersionFile() { + def versionFile = new File(rootDir, 'version') + if (!versionFile.isFile()) { + throw new Exception("Could not find version file") + } + if (versionFile.text.isEmpty()) { + throw new Exception("Version file does not contain a version") + } + versionFile.text.trim() +} diff --git a/build-logic/src/main/groovy/org.igniterealtime.smack.javadoc-conventions.gradle b/build-logic/src/main/groovy/org.igniterealtime.smack.javadoc-conventions.gradle new file mode 100644 index 000000000..61cb8f3c6 --- /dev/null +++ b/build-logic/src/main/groovy/org.igniterealtime.smack.javadoc-conventions.gradle @@ -0,0 +1,30 @@ +plugins { + // Javadoc linking requires repositories to bet configured. And + // those are declared in global-conventions, hence we add it here. + id 'org.igniterealtime.smack.global-conventions' +} + +if (JavaVersion.current().isJava8Compatible()) { + tasks.withType(Javadoc) { + // The '-quiet' as second argument is actually a hack, + // since the one parameter addStringOption doesn't seem to + // work, we extra add '-quiet', which is added anyway by + // gradle. + // We disable 'missing' as we do most of javadoc checking via checkstyle. + options.addStringOption('Xdoclint:all,-missing', '-quiet') + // Abort on javadoc warnings. + // See JDK-8200363 (https://bugs.openjdk.java.net/browse/JDK-8200363) + // for information about the -Xwerror option. + options.addStringOption('Xwerror', '-quiet') + } +} + +if (JavaVersion.current().isJava9Compatible()) { + tasks.withType(Javadoc) { + options.addStringOption('-release', javaMajor) + } +} + +tasks.withType(Javadoc) { + options.charSet = "UTF-8" +} diff --git a/build.gradle b/build.gradle index eda9ed85c..13f9b12d7 100644 --- a/build.gradle +++ b/build.gradle @@ -1,347 +1,20 @@ -buildscript { - repositories { - jcenter() - maven { url 'https://plugins.gradle.org/m2/' } - maven { url 'https://dl.bintray.com/content/aalmiray/kordamp' } - } - dependencies { - classpath 'org.kordamp.gradle:clirr-gradle-plugin:0.2.2' - classpath "biz.aQute.bnd:biz.aQute.bnd.gradle:6.0.0" - } -} - plugins { - id 'ru.vyarus.animalsniffer' version '1.5.0' - id 'net.ltgt.errorprone' version '1.3.0' - // Use e.g. "gradle taskTree" to show its dependency tree. - id 'com.dorongold.task-tree' version '1.5' - id 'com.github.kt3k.coveralls' version '2.10.2' + // The scalastyle plugin of smack-repl wants the root project to + // have a ideaProject task, so let's add one. + id 'idea' + + id 'org.igniterealtime.smack.javadoc-conventions' } ext { - java11Projects = [ + javadocAllDir = new File(buildDir, 'javadoc') + integrationTestProjects = [ ':smack-integration-test', ':smack-omemo-signal-integration-test', - ':smack-repl', - ':smack-websocket-java11', - ].collect { project(it) } - java11Projects += getRootProject() - java8Projects = allprojects - java11Projects + ].collect{ project(it) } + javadocAllProjects = subprojects - integrationTestProjects } -configure (java8Projects) { - ext { - javaCompatilibity = JavaVersion.VERSION_1_8 - } -} - -configure (java11Projects) { - ext { - javaCompatilibity = JavaVersion.VERSION_11 - } -} - -allprojects { - apply plugin: 'java-library' - apply plugin: 'java-test-fixtures' - apply plugin: 'eclipse' - apply plugin: 'idea' - apply plugin: 'jacoco' - apply plugin: 'net.ltgt.errorprone' - - version readVersionFile() - - ext { - isSnapshot = version.endsWith('-SNAPSHOT') - gitCommit = getGitCommit() - javadocAllDir = new File(buildDir, 'javadoc') - documentationDir = new File(projectDir, 'documentation') - releasedocsDir = new File(buildDir, 'releasedocs') - rootConfigDir = new File(rootDir, 'config') - sonatypeCredentialsAvailable = project.hasProperty('sonatypeUsername') && project.hasProperty('sonatypePassword') - isReleaseVersion = !isSnapshot - isContinuousIntegrationEnvironment = Boolean.parseBoolean(System.getenv('CI')) - signingRequired = !(isSnapshot || isContinuousIntegrationEnvironment) - sonatypeSnapshotUrl = 'https://oss.sonatype.org/content/repositories/snapshots' - sonatypeStagingUrl = 'https://oss.sonatype.org/service/local/staging/deploy/maven2' - // Returns only the date in yyyy-MM-dd format, as otherwise, with - // hh:mm:ss information, the manifest files would change with every - // build, causing unnecessary rebuilds. - builtDate = (new java.text.SimpleDateFormat("yyyy-MM-dd")).format(new Date()) - oneLineDesc = 'An Open Source XMPP (Jabber) client library' - integrationTestProjects = [ - ':smack-integration-test', - ':smack-omemo-signal-integration-test', - ].collect{ project(it) } - javadocAllProjects = subprojects - integrationTestProjects - // A dirty hack used for Gradle's jacoco plugin, since is not - // hable to handle the case when a (sub)project has no unit - // tests. :-( - projectsWithoutUnitTests = [ - ':smack-android', - ':smack-android-extensions', - ':smack-bosh', - ':smack-debug', - ':smack-debug-slf4j', - ':smack-java8', - ':smack-jingle-old', - ':smack-resolver-dnsjava', - ':smack-resolver-javax', - ':smack-resolver-minidns', - ':smack-omemo-signal-integration-test', - ].collect{ project(it) } - projectsWithUnitTests = subprojects - projectsWithoutUnitTests - androidProjects = [ - ':smack-tcp', - ':smack-bosh', - ':smack-core', - ':smack-im', - ':smack-resolver-minidns', - ':smack-sasl-provided', - ':smack-extensions', - ':smack-experimental', - ':smack-omemo', - ':smack-omemo-signal', - ':smack-openpgp', - ':smack-xmlparser', - ':smack-xmlparser-xpp3', - ].collect{ project(it) } - androidBootClasspathProjects = [ - ':smack-android', - ':smack-android-extensions', - ].collect{ project(it) } - androidOptionalProjects = [ - ':smack-tcp', - ':smack-extensions', - ':smack-experimental', - ':smack-bosh', - ':smack-omemo', - ':smack-omemo-signal', - ].collect{ project(it) } - gplLicensedProjects = [ - ':smack-omemo-signal', - ':smack-omemo-signal-integration-test', - ':smack-repl' - ].collect{ project(it) } - // Lazily evaluate the Android bootClasspath and offline - // Javadoc using a closure, so that targets which do not - // require it are still able to succeed without an Android - // SDK. - androidBootClasspath = { getAndroidRuntimeJar() } - androidJavadocOffline = { getAndroidJavadocOffline() } - junit4Projects = [ - ':smack-core', - ':smack-im', - ':smack-omemo', - ':smack-omemo-signal', - ':smack-openpgp', - ].collect { project(it) } - - // When using dynamic versions for those, do *not* use [1.0, - // 2.0), since this will also pull in 2.0-alpha1. Instead use - // [1.0, 1.0.99]. - // See also: - // - https://issues.apache.org/jira/browse/MNG-6232 - // - https://issues.igniterealtime.org/browse/SMACK-858 - jxmppVersion = '[1.0.0, 1.0.999]' - miniDnsVersion = '[1.0.0, 1.0.999]' - smackMinAndroidSdk = 19 - junitVersion = '5.7.1' - commonsIoVersion = '2.6' - bouncyCastleVersion = '1.71' - guavaVersion = '30.1-jre' - mockitoVersion = '3.7.7' - orgReflectionsVersion = '0.9.11' - - if (project.hasProperty("useSonatype")) { - useSonatype = project.getProperty("useSonatype").toBoolean() - } else { - // Default to true - useSonatype = true - } - javaMajor = javaCompatilibity.getMajorVersion() - } - group = 'org.igniterealtime.smack' - sourceCompatibility = javaCompatilibity - targetCompatibility = sourceCompatibility - - test { - useJUnitPlatform() - - maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1 - - // Enable full stacktraces of failed tests. Especially handy - // for environments like Travis. - testLogging { - events "failed" - exceptionFormat "full" - } - } - - ext.sharedManifest = manifest { - attributes('Implementation-Version': version, - 'Implementation-GitRevision': ext.gitCommit, - 'Built-Date': ext.builtDate, - 'Built-JDK': System.getProperty('java.version'), - 'Built-Gradle': gradle.gradleVersion, - 'Built-By': System.getProperty('user.name') - ) - } - - eclipse { - classpath { - downloadJavadoc = true - } - } - - repositories { - mavenLocal() - mavenCentral() - } - - tasks.withType(JavaCompile) { - // Some systems may not have set their platform default - // converter to 'utf8', but we use unicode in our source - // files. Therefore ensure that javac uses unicode - options.encoding = 'UTF-8' - options.compilerArgs = [ - '-Xlint:all', - // Set '-options' because a non-java7 javac will emit a - // warning if source/traget is set to 1.7 and - // bootclasspath is *not* set. - // TODO implement a sound heuristic to determine a java7 - // rt.jar on the build host. And if none is found, - // fallback to using a environment variable, - // e.g. JAVA7_HOME. See SMACK-651. - '-Xlint:-options', - '-Werror', - ] - options.errorprone { - error( - "UnusedVariable", - "UnusedMethod", - "MethodCanBeStatic", - ) - errorproneArgs = [ - // Disable errorprone checks - '-Xep:TypeParameterUnusedInFormals:OFF', - // Disable errorpone StringSplitter check, as it - // recommends using Splitter from Guava, which we don't - // have (nor want to use in Smack). - '-Xep:StringSplitter:OFF', - '-Xep:JdkObsolete:OFF', - // Disabled because sinttest re-uses BeforeClass from junit. - // TODO: change sinttest so that it has it's own - // BeforeClass and re-enable this check. - '-Xep:JUnit4ClassAnnotationNonStatic:OFF', - // Disabled but should be re-enabled at some point - //'-Xep:InconsistentCapitalization:OFF', - '-Xep:MixedMutabilityReturnType:OFF', - // TODO: Re-enable once Smack's minimum Android SDK level is 26 or higher. - '-Xep:JavaUtilDate:OFF', - ] - } - } - - tasks.withType(ScalaCompile) { - scalaCompileOptions.additionalParameters = [ - '-Xfatal-warnings', - '-feature', - ] - } - - jacoco { - toolVersion = "0.8.6" - } - - jacocoTestReport { - dependsOn test - getSourceDirectories().setFrom(project.files(sourceSets.main.allSource.srcDirs)) - getClassDirectories().setFrom(project.files(sourceSets.main.output)) - reports { - xml.enabled true - } - } - - if (JavaVersion.current().isJava8Compatible()) { - tasks.withType(Javadoc) { - // The '-quiet' as second argument is actually a hack, - // since the one paramater addStringOption doesn't seem to - // work, we extra add '-quiet', which is added anyway by - // gradle. - // TODO: This enables all doclint check but - // 'missing'. Re-enable 'missing' once every public API in - // Smack has a javadoc comment. - options.addStringOption('Xdoclint:accessibility,html,reference,syntax', '-quiet') - - // Treat warnings as errors. - // See also https://bugs.openjdk.java.net/browse/JDK-8200363 - options.addStringOption('Xwerror', '-quiet') - } - } - - if (JavaVersion.current().isJava9Compatible()) { - tasks.withType(Javadoc) { - options.addStringOption('-release', javaMajor) - - // The -no-modules-directories javadoc option was removed in Java 13 - // See https://bugs.openjdk.java.net/browse/JDK-8215582 - if (JavaVersion.current() < JavaVersion.VERSION_13) { - // Fix for javadoc search. If not set, the search result would direct to - // javadoc/undefined/org/jivesoftware/smack/altconnections/HttpLookupMethod.html - // instead of - // javadoc/org/jivesoftware/smack/altconnections/HttpLookupMethod.html - // https://stackoverflow.com/a/53732633/194894 - options.addBooleanOption("-no-module-directories", true) - } - } - tasks.withType(JavaCompile) { - options.compilerArgs.addAll([ - '--release', javaMajor, - ]) - } - } - -tasks.withType(Javadoc) { - options.charSet = "UTF-8" - options.encoding = 'UTF-8' - } - - dependencies { - testFixturesApi "org.junit.jupiter:junit-jupiter-api:$junitVersion" - testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion" - testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" - - // The smack-extensions subproject uses mockito in its fest - // fixtures, and we want to have mockito also available in - // test, so we use API here. - testFixturesApi "org.mockito:mockito-core:${mockitoVersion}" - - // To mock final classes - testImplementation "org.mockito:mockito-inline:${mockitoVersion}" - testImplementation 'com.jamesmurty.utils:java-xmlbuilder:1.2' - - errorprone 'com.google.errorprone:error_prone_core:2.5.1' - errorproneJavac('com.google.errorprone:javac:9+181-r4173-1') - } - - // Make all project's 'test' target depend on javadoc, so that - // javadoc is also linted. - test { dependsOn javadoc } -} - -configure (junit4Projects) { - dependencies { - testImplementation "org.junit.vintage:junit-vintage-engine:$junitVersion" - } -} - -// We need to evaluate the child projects first because -// - javadocAll needs the smack-core child to have already resolved -// the jXMPP/MiniDNS dependencies, so that we can the resovled -// version to link to those project's javadoc. -// - We use the child's project description as description for the -// Maven POM. evaluationDependsOnChildren() task javadocAll(type: Javadoc) { source javadocAllProjects.collect {project -> @@ -390,315 +63,6 @@ task javadocAll(type: Javadoc) { } } -import org.apache.tools.ant.filters.ReplaceTokens -task prepareReleasedocs(type: Copy) { - from 'resources/releasedocs' - into releasedocsDir - filter(ReplaceTokens, tokens: [version: version, releasedate: builtDate, targetCompatibility: targetCompatibility.toString()]) -} - -task distributionZip(type: Zip, dependsOn: [javadocAll, prepareReleasedocs]) { - classifier builtDate - into ('javadoc') { - from(javadocAllDir) - } - into ('releasedocs') { - from(releasedocsDir) - } - into ('releasedocs/documentation') { - from(documentationDir) - } -} - -task maybeCheckForSnapshotDependencies { - // Don't check for Snapshot dependencies if this is a snapshot. - onlyIf { isReleaseVersion } - // Run in the execution phase, not in configuration phase, as the - // 'each' forces the runtime configuration to be resovled, which - // causes "Cannot change dependencies of configuration after it - // has been included in dependency resolution." errors. - // See https://discuss.gradle.org/t/23153 - doLast { - allprojects { project -> - project.configurations.runtime.each { - if (it.toString().contains("-SNAPSHOT")) - throw new Exception("Release build contains snapshot dependencies: " + it) - } - } - } -} - -test { dependsOn maybeCheckForSnapshotDependencies } - -jar { - // Root project should not create empty jar artifact - enabled = false -} - -// Disable upload archives for the root project -uploadArchives.enabled = false - -description = """\ -Smack ${version} -${oneLineDesc}.""" - -subprojects { - apply plugin: 'maven-publish' - apply plugin: 'signing' - apply plugin: 'checkstyle' - apply plugin: 'org.kordamp.gradle.clirr' - apply plugin: 'biz.aQute.bnd.builder' - - checkstyle { - toolVersion = '8.27' - } - task sourcesJar(type: Jar, dependsOn: classes) { - classifier = 'sources' - from sourceSets.main.allSource - } - task javadocJar(type: Jar, dependsOn: javadoc) { - classifier = 'javadoc' - from javadoc.destinationDir - } - task testsJar(type: Jar, dependsOn: testClasses) { - classifier = 'tests' - from sourceSets.test.output - } - - artifacts { - // See http://stackoverflow.com/a/21946676/194894 - testRuntime testsJar - } - - publishing { - publications { - mavenJava(MavenPublication) { - from components.java - artifact sourcesJar - artifact javadocJar - artifact testsJar - pom { - name = 'Smack' - packaging = 'jar' - inceptionYear = '2003' - url = 'http://www.igniterealtime.org/projects/smack/' - description = project.description - - issueManagement { - system = 'JIRA' - url = 'https://igniterealtime.org/issues/browse/SMACK' - } - - scm { - url = 'https://github.com/igniterealtime/Smack' - connection = 'scm:git:https://github.com/igniterealtime/Smack.git' - developerConnection = 'scm:git:https://github.com/igniterealtime/Smack.git' - } - - developers { - developer { - id = 'flow' - name = 'Florian Schmaus' - email = 'flow@igniterealtime.org' - } - } - } - } - } - repositories { - if (sonatypeCredentialsAvailable && useSonatype) { - maven { - url isSnapshot ? sonatypeSnapshotUrl : sonatypeStagingUrl - credentials { - username = sonatypeUsername - password = sonatypePassword - } - } - } - // Use - // gradle publish -P customRepoUrl=https://www.igniterealtime.org/archiva/repository/maven -P customRepoUsername=bamboo -P customRepoPassword=hidden -P useSonatype=false - // to deploy to this repo. - if (project.hasProperty("customRepoUrl")) { - maven { - name 'customRepo' - url customRepoUrl - if (project.hasProperty("customRepoUsername")) { - credentials { - username customRepoUsername - password customRepoPassword - } - } - } - } - } - } - rootProject.distributionZip { - dependsOn build - from(buildDir) { - include "$libsDirName/*${version}.jar" - include "$libsDirName/*${version}-javadoc.jar" - include "$libsDirName/*${version}-sources.jar" - } - } - - // Workaround for gpg signatory not supporting the 'required' option - // See https://github.com/gradle/gradle/issues/5064#issuecomment-381924984 - // Note what we use 'signing.gnupg.keyName' instead of 'signing.keyId'. - tasks.withType(Sign) { - onlyIf { - project.hasProperty('signing.gnupg.keyName') - } - } - signing { - useGpgCmd() - required { signingRequired } - sign publishing.publications.mavenJava - } - - clirr { - // 2018-08-14: Disabled Clirr because - // - It reports an breaking change in android.jar (seems right, but there is nothing we can do about it) - // - Only the first smack-* projects are correctly checked, - // the other ones have the output of a clirr report from a previous project - // (Look at the clirr reports). - enabled false - semver false - } - - // Work around https://github.com/gradle/gradle/issues/4046 - task copyJavadocDocFiles(type: Copy) { - from('src/javadoc') - into 'build/docs/javadoc' - include '**/doc-files/*.*' - } - javadoc.dependsOn copyJavadocDocFiles - - // Make sure root projects 'javadocAll' depends on the - // subproject's javadoc, to ensure that all all doc-files/ are - // generated and up-to-date. Obviously this means that the - // javadocAll task will also create the individual javadoc's of the - // subprojects. - javadocAll.dependsOn javadoc -} - -// The smack-java8-full project generates the dot and png files of the -// current state graph. Ensure they are generated before copied. -configure (project(':smack-java8-full')) { - copyJavadocDocFiles.dependsOn convertModularXmppClientToServerConnectionStateGraphDotToPng -} - -configure (androidProjects + androidBootClasspathProjects) { - apply plugin: 'ru.vyarus.animalsniffer' - dependencies { - signature "net.sf.androidscents.signature:android-api-level-${smackMinAndroidSdk}:4.4.2_r4@signature" - } - animalsniffer { - sourceSets = [sourceSets.main] - } -} - -// There is no need to ever clirr integration test projects and the -// smack-repl project. -configure(integrationTestProjects + project(':smack-repl')) { - clirr { - enabled false - } -} - -// Disable clirr on omemo modules -project(':smack-omemo').clirr.enabled = false -project(':smack-omemo-signal').clirr.enabled = false - -subprojects*.jar { - manifest { - from sharedManifest - } - bundle { - bnd( - '-removeheaders': 'Tool, Bnd-*', - '-exportcontents': '*', - ) - } -} - -configure(subprojects - gplLicensedProjects) { - checkstyle { - configProperties.checkstyleLicenseHeader = "header" - } - publishing { - publications { - mavenJava(MavenPublication) { - pom { - licenses { - license { - name = 'The Apache Software License, Version 2.0' - url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' - distribution = 'repo' - } - } - } - } - } - } -} - -configure(gplLicensedProjects) { - checkstyle { - configProperties.checkstyleLicenseHeader = "${project.name}-gplv3-license-header" - } - publishing { - publications { - mavenJava(MavenPublication) { - pom { - licenses { - license { - name = 'GNU General Public License, version 3 or any later version' - url = 'https://www.gnu.org/licenses/gpl.txt' - distribution = 'repo' - } - } - } - } - } - } -} - -configure(androidBootClasspathProjects) { - compileJava { - options.bootstrapClasspath = files(androidBootClasspath) - } - javadoc { - classpath += files(androidBootClasspath) - } -} - -apply plugin: "com.github.kt3k.coveralls" -coveralls { - sourceDirs = files(subprojects.sourceSets.main.allSource.srcDirs).files.absolutePath -} - -task jacocoRootReport(type: org.gradle.testing.jacoco.tasks.JacocoReport) { - dependsOn = projectsWithUnitTests.jacocoTestReport - getSourceDirectories().setFrom(files(projectsWithUnitTests.sourceSets.main.allSource.srcDirs)) - getClassDirectories().setFrom(files(projectsWithUnitTests.sourceSets.main.output)) - getExecutionData().setFrom(files(projectsWithUnitTests.jacocoTestReport.executionData)) - reports { - xml.enabled true - xml.destination file("${buildDir}/reports/jacoco/test/jacocoTestReport.xml") - } - // We could remove the following setOnlyIf line, but then - // jacocoRootReport would silently be SKIPPED if something with - // the projectsWithUnitTests is wrong (e.g. a project is missing - // in there). - setOnlyIf { true } -} - -// Important to specify this task after the subprojects block -task clirrRootReport(type: org.kordamp.gradle.clirr.ClirrReportTask) { - dependsOn = subprojects.tasks.clirr - reports = files((subprojects.findAll { it.clirr.enabled == true }).tasks.clirr.xmlReport) -} - task integrationTest { description 'Verify correct functionality of Smack by running some integration tests.' dependsOn project(':smack-integration-test').tasks.run @@ -706,7 +70,7 @@ task integrationTest { task omemoSignalIntTest { description 'Run integration tests of the smack-omemo module in combination with smack-omemo-signal.' - dependsOn project(':smack-omemo-signal-integration-test').tasks.run + dependsOn 'smack-omemo-signal-integration-test:run' } task sinttestAll { @@ -717,70 +81,6 @@ task sinttestAll { ]} } -def getGitCommit() { - def projectDirFile = new File("$projectDir") - def dotGit = new File(projectDirFile, ".git") - if (!dotGit.isDirectory()) return 'non-git build' - - def cmd = 'git describe --always --tags --dirty=+' - def proc = cmd.execute(null, projectDirFile) - proc.waitForOrKill(10 * 1000) - - def gitCommit = proc.text.trim() - assert !gitCommit.isEmpty() - - def srCmd = 'git symbolic-ref --short HEAD' - def srProc = srCmd.execute(null, projectDirFile) - srProc.waitForOrKill(10 * 1000) - if (srProc.exitValue() == 0) { - // Only add the information if the git command was - // successful. There may be no symbolic reference for HEAD if - // e.g. in detached mode. - def symbolicReference = srProc.text.trim() - assert !symbolicReference.isEmpty() - gitCommit += "-$symbolicReference" - } - - gitCommit -} - -def getAndroidRuntimeJar() { - def androidApiLevel = ext.smackMinAndroidSdk - def androidHome = getAndroidHome() - def androidJar = new File("$androidHome/platforms/android-${androidApiLevel}/android.jar") - if (androidJar.isFile()) { - return androidJar - } else { - throw new Exception("Can't find android.jar for API level ${androidApiLevel}. Please install corresponding SDK platform package") - } -} - -def getAndroidJavadocOffline() { - def androidHome = getAndroidHome() - return androidHome.toString() + "/docs/reference" -} - -def getAndroidHome() { - def androidHomeEnv = System.getenv("ANDROID_HOME") - if (androidHomeEnv == null) { - throw new Exception("ANDROID_HOME environment variable is not set") - } - def androidHome = new File(androidHomeEnv) - if (!androidHome.isDirectory()) throw new Exception("Environment variable ANDROID_HOME is not pointing to a directory") - return androidHome -} - -def readVersionFile() { - def versionFile = new File(rootDir, 'version') - if (!versionFile.isFile()) { - throw new Exception("Could not find version file") - } - if (versionFile.text.isEmpty()) { - throw new Exception("Version file does not contain a version") - } - versionFile.text.trim() -} - def getResolvedVersion(queriedProject = 'smack-core', component) { def configuration = project(queriedProject) .configurations @@ -791,7 +91,7 @@ def getResolvedVersion(queriedProject = 'smack-core', component) { .resolvedArtifacts .findAll { // 'it' is of type ResolvedArtifact, 'id' of - // Component*Artifcat*Identifier, and we check the + // Component*Artifact*Identifier, and we check the // ComponentIdentifier. it.id.getComponentIdentifier() instanceof org.gradle.api.artifacts.component.ModuleComponentIdentifier } diff --git a/config/checkstyle/smack-examples-gplv3-license-header.txt b/config/checkstyle/smack-examples-gplv3-license-header.txt new file mode 100644 index 000000000..3d9d392c4 --- /dev/null +++ b/config/checkstyle/smack-examples-gplv3-license-header.txt @@ -0,0 +1,20 @@ +/** + * + * Copyright 20XX John Doe + * + * 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 + */ diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 000000000..671a138d8 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,5 @@ +# Workaround for https://github.com/CycloneDX/cyclonedx-gradle-plugin/issues/349 +# suggested at https://docs.gradle.org/current/userguide/upgrading_version_8.html#xml_parsing_now_requires_recent_parsers +systemProp.javax.xml.parsers.SAXParserFactory=com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl +systemProp.javax.xml.transform.TransformerFactory=com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl +systemProp.javax.xml.parsers.DocumentBuilderFactory=com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e708b1c02..a4b76b953 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 442d9132e..df97d72b8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 4f906e0c8..f5feea6d6 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,69 +15,104 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +122,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,88 +133,120 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=`expr $i + 1` - done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index ac1b06f93..9b42019c7 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,8 +13,10 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +27,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -56,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -75,13 +78,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/repl b/repl index c6014bbb4..e31926a9d 100755 --- a/repl +++ b/repl @@ -1,7 +1,5 @@ #!/usr/bin/env bash -set -e -set -u -set -o pipefail +set -euo pipefail JDWP=false JDWP_PORT=8000 @@ -37,12 +35,13 @@ echo "Compiling and computing classpath (May take a while)" # /smack/smack-repl/build/classes/main:/smack/smack-repl/build/ # resources/main:/smack/smack-tcp/build/libs/smack-tcp-4.2.0-alpha4-SNAPSHOT.jar # So perform a "tail -n1" on the output of gradle -GRADLE_CLASSPATH="$(gradle :smack-repl:printClasspath --quiet |\ +GRADLE_CLASSPATH="$(${GRADLE_BIN:-./gradlew} :smack-repl:printClasspath --quiet |\ tail -n1)" echo "Finished, starting REPL" -java "${EXTRA_JAVA_ARGS[@]}" \ +exec java \ + "${EXTRA_JAVA_ARGS[@]}" \ -Dscala.usejavacp=true \ -classpath "${GRADLE_CLASSPATH}" \ ammonite.Main \ - --predef "smack-repl/scala.repl" + --predef smack-repl/scala.repl diff --git a/resources/fix-a-javadoc.sh b/resources/fix-a-javadoc.sh index 94e8f31fd..a300a1a59 100755 --- a/resources/fix-a-javadoc.sh +++ b/resources/fix-a-javadoc.sh @@ -29,7 +29,7 @@ SMACK_EXCEPTIONS[SmackException]="if Smack detected an exceptional situation." SMACK_EXCEPTIONS[XMPPException]="if an XMPP protocol error was received." SMACK_EXCEPTIONS[SmackSaslException]="if a SASL specific error occurred." SMACK_EXCEPTIONS[SASLErrorException]="if a SASL protocol error was returned." -SMACK_EXCEPTIONS[NotAMucServiceException]="if the entity is not a MUC serivce." +SMACK_EXCEPTIONS[NotAMucServiceException]="if the entity is not a MUC service." SMACK_EXCEPTIONS[NoSuchAlgorithmException]="if no such algorithm is available." SMACK_EXCEPTIONS[KeyManagementException]="if there was a key mangement error." SMACK_EXCEPTIONS[XmppStringprepException]="if the provided string is invalid." @@ -53,7 +53,7 @@ SMACK_EXCEPTIONS[Exception]="if an exception occurred." SMACK_EXCEPTIONS[TestNotPossibleException]="if the test is not possible." SMACK_EXCEPTIONS[TimeoutException]="if there was a timeout." SMACK_EXCEPTIONS[IllegalStateException]="if an illegal state was encountered" -SMACK_EXCEPTIONS[NoSuchPaddingException]="if the requested padding mechanism is not availble." +SMACK_EXCEPTIONS[NoSuchPaddingException]="if the requested padding mechanism is not available." SMACK_EXCEPTIONS[BadPaddingException]="if the input data is not padded properly." SMACK_EXCEPTIONS[InvalidKeyException]="if the key is invalid." SMACK_EXCEPTIONS[IllegalBlockSizeException]="if the input data length is incorrect." diff --git a/resources/getCopyright.sh b/resources/getCopyright.sh index c34fd87da..fdde88b0f 100755 --- a/resources/getCopyright.sh +++ b/resources/getCopyright.sh @@ -15,7 +15,7 @@ for p in $SUBPROJECTS; do sort | \ # Remove duplicates uniq | \ - # Split multi Copyright statemtents, e.g. "2001-2013 FooBar, 2014 Baz" + # Split multi Copyright statements, e.g. "2001-2013 FooBar, 2014 Baz" tr ',' '\n' | \ # Remove whitespaces resulting from the previous split sed "s/^[ \t]*//" | \ diff --git a/resources/releasedocs/README.html b/resources/releasedocs/README.html deleted file mode 100644 index d25eaf624..000000000 --- a/resources/releasedocs/README.html +++ /dev/null @@ -1,221 +0,0 @@ - - - - - Smack Readme - - - - -
- - - - -
- -

- - - - - - - - - -
version:@version@
released:@releasedate@
- -

-Thank you for downloading Smack! This version of Smack is compatible -with JVMs @targetCompatibility@ or higher. Using a build system which -is able to consume Maven artifacts, like gradle or Maven, is highly -recommended when using Smack. -

- -

- This is not the real README. Please visit -

- https://www.igniterealtime.org/projects/smack/readme -
- for the README of the current stable Smack version. -

- -

- Smack tries to depend on as few as possible libraries. The only - requirement is jXMPP. For DNS - resolution we recommend to - use MiniDNS. -

- -

-Start off by viewing the documentation -that can be found in the "documentation" directory included with this distribution. -

-Further information can be found on the -Smack website. If you need help using or would like to make contributions or -fixes to the code, please visit the -online forum. -

- -

Changelog and Upgrading

- -View the changelog for a list of changes since the -last release. - -

License Agreements

-

    -
  • Use of the Smack source code is governed by the Apache License Version 2.0: -
    - Copyright 2002-2008 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.
    - 
  • - -
  • Smack contains icons and images licensed from INCORS GmbH. You are not licensed -to use these icons outside of Smack.
  • - -
  • Third-party source code is licensed as noted in their source files. - -
-
-
- - diff --git a/settings.gradle b/settings.gradle index 91c403b77..d433b95d3 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,7 @@ +pluginManagement { + includeBuild('build-logic') +} + // The name of the root project. // If we would not set the name, then gradle would use the directory // name of the root directory @@ -6,6 +10,7 @@ rootProject.name = 'Smack' include 'smack-core', 'smack-im', 'smack-tcp', + 'smack-examples', 'smack-extensions', 'smack-experimental', 'smack-debug', @@ -22,8 +27,8 @@ include 'smack-core', 'smack-bosh', 'smack-android', 'smack-android-extensions', - 'smack-java8', - 'smack-java8-full', + 'smack-java11', + 'smack-java11-full', 'smack-integration-test', 'smack-omemo', 'smack-omemo-signal', diff --git a/smack-android-extensions/build.gradle b/smack-android-extensions/build.gradle index 0cdad8bda..ce9a40fc4 100644 --- a/smack-android-extensions/build.gradle +++ b/smack-android-extensions/build.gradle @@ -1,3 +1,9 @@ +plugins { + id 'org.igniterealtime.smack.java-common-conventions' + id 'org.igniterealtime.smack.android-conventions' + id 'org.igniterealtime.smack.android-boot-classpath-conventions' +} + description = """\ Extra Smack extensions for Android.""" @@ -8,5 +14,5 @@ dependencies { api project(':smack-extensions') // Add the Android jar to the Eclipse .classpath. - compileClasspath files(androidBootClasspath) + implementation files(androidBootClasspath) } diff --git a/smack-android-extensions/src/main/java/org/jivesoftware/smackx/package-info.java b/smack-android-extensions/src/main/java/org/jivesoftware/smackx/package-info.java index 7ebe86244..bce3b6064 120000 --- a/smack-android-extensions/src/main/java/org/jivesoftware/smackx/package-info.java +++ b/smack-android-extensions/src/main/java/org/jivesoftware/smackx/package-info.java @@ -1 +1 @@ -../../../../../../../smack-java8-full/src/main/java/org/jivesoftware/smackx/package-info.java \ No newline at end of file +../../../../../../../smack-java11-full/src/main/java/org/jivesoftware/smackx/package-info.java \ No newline at end of file diff --git a/smack-android-extensions/src/main/java/org/jivesoftware/smackx/ping/android/ServerPingWithAlarmManager.java b/smack-android-extensions/src/main/java/org/jivesoftware/smackx/ping/android/ServerPingWithAlarmManager.java index a81f51ade..0315629c8 100644 --- a/smack-android-extensions/src/main/java/org/jivesoftware/smackx/ping/android/ServerPingWithAlarmManager.java +++ b/smack-android-extensions/src/main/java/org/jivesoftware/smackx/ping/android/ServerPingWithAlarmManager.java @@ -1,6 +1,6 @@ /** * - * Copyright © 2014-2021 Florian Schmaus + * Copyright © 2014-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. @@ -36,6 +36,7 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.os.Build; import android.os.SystemClock; /** @@ -121,6 +122,7 @@ public final class ServerPingWithAlarmManager extends Manager { private static final BroadcastReceiver ALARM_BROADCAST_RECEIVER = new BroadcastReceiver() { @Override + @SuppressWarnings("LockOnNonEnclosingClassLiteral") public void onReceive(Context context, Intent intent) { LOGGER.fine("Ping Alarm broadcast received"); Set> managers; @@ -163,7 +165,7 @@ public final class ServerPingWithAlarmManager extends Manager { private static AlarmManager sAlarmManager; /** - * Register a pending intent with the AlarmManager to be broadcasted every half hour and + * Register a pending intent with the AlarmManager to be broadcast every half hour and * register the alarm broadcast receiver to receive this intent. The receiver will check all * known questions if a ping is Necessary when invoked by the alarm intent. * @@ -173,7 +175,11 @@ public final class ServerPingWithAlarmManager extends Manager { sContext = context; context.registerReceiver(ALARM_BROADCAST_RECEIVER, new IntentFilter(PING_ALARM_ACTION)); sAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - sPendingIntent = PendingIntent.getBroadcast(context, 0, new Intent(PING_ALARM_ACTION), 0); + int pendingIntentFlags = 0; + if (Build.VERSION.SDK_INT >= 23) { + pendingIntentFlags |= PendingIntent.FLAG_IMMUTABLE; + } + sPendingIntent = PendingIntent.getBroadcast(context, 0, new Intent(PING_ALARM_ACTION), pendingIntentFlags); sAlarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + AlarmManager.INTERVAL_HALF_HOUR, AlarmManager.INTERVAL_HALF_HOUR, sPendingIntent); diff --git a/smack-android/build.gradle b/smack-android/build.gradle index 556a526eb..ef158e137 100644 --- a/smack-android/build.gradle +++ b/smack-android/build.gradle @@ -1,3 +1,9 @@ +plugins { + id 'org.igniterealtime.smack.java-common-conventions' + id 'org.igniterealtime.smack.android-conventions' + id 'org.igniterealtime.smack.android-boot-classpath-conventions' +} + description = """\ Smack for Android. All the required dependencies to run Smack on Android. @@ -16,13 +22,13 @@ dependencies { // used in non-Android projects. implementation "org.minidns:minidns-android21:$miniDnsVersion" - // androidProjects lists all projects that are checked to compile against android.jar - // Filter out the optional Smack dependencies from androidProjects - (androidProjects - androidOptionalProjects) - .each { project -> - api project - } + api project(':smack-core') + api project(':smack-im') + api project(':smack-resolver-minidns') + api project(':smack-sasl-provided') + api project(':smack-xmlparser') + api project(':smack-xmlparser-xpp3') // Add the Android jar to the Eclipse .classpath. - compileClasspath files(androidBootClasspath) + implementation files(androidBootClasspath) } diff --git a/smack-android/src/main/java/org/jivesoftware/smack/android/AndroidSmackInitializer.java b/smack-android/src/main/java/org/jivesoftware/smack/android/AndroidSmackInitializer.java index a34fb1776..b66b56462 100644 --- a/smack-android/src/main/java/org/jivesoftware/smack/android/AndroidSmackInitializer.java +++ b/smack-android/src/main/java/org/jivesoftware/smack/android/AndroidSmackInitializer.java @@ -37,6 +37,8 @@ import org.minidns.dnsserverlookup.android21.AndroidUsingLinkProperties; public class AndroidSmackInitializer implements SmackInitializer { @Override + // Android deprecated StrictHostnameVerifier in API level 22 + @SuppressWarnings("deprecation") public List initialize() { SmackConfiguration.setDefaultHostnameVerifier(new StrictHostnameVerifier()); Base64.setEncoder(AndroidBase64Encoder.INSTANCE); diff --git a/smack-android/src/main/java/org/jivesoftware/smackx/debugger/android/AndroidDebugger.java b/smack-android/src/main/java/org/jivesoftware/smackx/debugger/android/AndroidDebugger.java index 068dab202..930fb9195 100644 --- a/smack-android/src/main/java/org/jivesoftware/smackx/debugger/android/AndroidDebugger.java +++ b/smack-android/src/main/java/org/jivesoftware/smackx/debugger/android/AndroidDebugger.java @@ -28,7 +28,7 @@ import android.util.Log; * implementation, therefore {@link org.jivesoftware.smack.debugger.JulDebugger} is preferred. *

* It is possible to not only print the raw sent and received stanzas but also the interpreted - * packets by Smack. By default interpreted packets won't be printed. To enable this feature + * packets by Smack. By default,interpreted packets won't be printed. To enable this feature * just change the printInterpreted static variable to true. * */ diff --git a/smack-android/src/main/java/org/jivesoftware/smackx/package-info.java b/smack-android/src/main/java/org/jivesoftware/smackx/package-info.java index 7ebe86244..bce3b6064 120000 --- a/smack-android/src/main/java/org/jivesoftware/smackx/package-info.java +++ b/smack-android/src/main/java/org/jivesoftware/smackx/package-info.java @@ -1 +1 @@ -../../../../../../../smack-java8-full/src/main/java/org/jivesoftware/smackx/package-info.java \ No newline at end of file +../../../../../../../smack-java11-full/src/main/java/org/jivesoftware/smackx/package-info.java \ No newline at end of file diff --git a/smack-bosh/build.gradle b/smack-bosh/build.gradle index fcd3d5896..d939a9e46 100644 --- a/smack-bosh/build.gradle +++ b/smack-bosh/build.gradle @@ -1,3 +1,8 @@ +plugins { + id 'org.igniterealtime.smack.java-common-conventions' + id 'org.igniterealtime.smack.android-conventions' +} + description = """\ Smack BOSH API. This API is considered beta quality.""" diff --git a/smack-bosh/src/main/java/org/jivesoftware/smack/bosh/XMPPBOSHConnection.java b/smack-bosh/src/main/java/org/jivesoftware/smack/bosh/XMPPBOSHConnection.java index 14d7c0e90..f1c0b9015 100644 --- a/smack-bosh/src/main/java/org/jivesoftware/smack/bosh/XMPPBOSHConnection.java +++ b/smack-bosh/src/main/java/org/jivesoftware/smack/bosh/XMPPBOSHConnection.java @@ -172,14 +172,15 @@ public class XMPPBOSHConnection extends AbstractXMPPConnection { client = BOSHClient.create(cfgBuilder.build()); - client.addBOSHClientConnListener(new BOSHConnectionListener()); - client.addBOSHClientResponseListener(new BOSHPacketReader()); - - // Initialize the debugger + // Initialize the debugger before addBOSHClientResponseListener(new BOSHPacketReader()); + // BOSHPacketReader may hold and send response prior to display of the request i.e. before if (debugger != null) { initDebugger(); } + client.addBOSHClientConnListener(new BOSHConnectionListener()); + client.addBOSHClientResponseListener(new BOSHPacketReader()); + // Send the session creation request client.send(ComposableBody.builder() .setNamespaceDefinition("xmpp", XMPP_BOSH_NS) @@ -359,10 +360,11 @@ public class XMPPBOSHConnection extends AbstractXMPPConnection { CloseableUtil.maybeClose(reader, LOGGER); CloseableUtil.maybeClose(writer, LOGGER); + // set readerConsumer = null before reader to avoid NPE reference + readerConsumer = null; readerPipe = null; reader = null; writer = null; - readerConsumer = null; } /** @@ -440,6 +442,8 @@ public class XMPPBOSHConnection extends AbstractXMPPConnection { if (event.getBody() != null) { try { writer.write(event.getBody().toXML()); + // Fix all BOSH sent debug messages not shown + writer.flush(); } catch (Exception e) { // Ignore } diff --git a/smack-core/build.gradle b/smack-core/build.gradle index f449ec5b7..1ffb92eb7 100644 --- a/smack-core/build.gradle +++ b/smack-core/build.gradle @@ -1,7 +1,7 @@ -// Note that this is also declared in the main build.gradle for -// subprojects, but since evaluationDependsOnChildren is enabled we -// need to declare it here too to have bundle{bnd{...}} available -apply plugin: 'biz.aQute.bnd.builder' +plugins { + id 'org.igniterealtime.smack.java-common-conventions' + id 'org.igniterealtime.smack.android-conventions' +} description = """\ Smack core components.""" @@ -16,6 +16,9 @@ dependencies { api "org.jxmpp:jxmpp-jid:$jxmppVersion" api "org.minidns:minidns-core:$miniDnsVersion" + // TODO: Migrate Junit4 tests to Junit5. + testImplementation "org.junit.vintage:junit-vintage-engine:$junitVersion" + testFixturesImplementation project(':smack-xmlparser-stax') testFixturesImplementation project(':smack-xmlparser-xpp3') @@ -28,7 +31,7 @@ dependencies { testFixturesApi "org.jxmpp:jxmpp-jid:$jxmppVersion:tests" testFixturesApi "org.xmlunit:xmlunit-core:$xmlUnitVersion" - // Explictily add assertj-core which is a dependency of + // Explicitly add assertj-core which is a dependency of // xmlunit-assertj, but gradle fails to resolves it with: // Execution failed for task ':smack-core:compileTestJava'. // > Could not resolve all files for configuration ':smack-core:testCompileClasspath'. @@ -59,7 +62,7 @@ task createVersionResource(type: CreateFileTask) { outputFile = new File(projectDir, 'src/main/resources/org.jivesoftware.smack/version') } -compileJava.dependsOn(createVersionResource) +processResources.dependsOn(createVersionResource) jar { bundle { diff --git a/smack-core/src/integration-test/java/org/jivesoftware/smack/MessageTest.java b/smack-core/src/integration-test/java/org/jivesoftware/smack/MessageTest.java index 75b4f6915..bcded200c 100644 --- a/smack-core/src/integration-test/java/org/jivesoftware/smack/MessageTest.java +++ b/smack-core/src/integration-test/java/org/jivesoftware/smack/MessageTest.java @@ -60,7 +60,7 @@ public class MessageTest extends SmackTestCase { } Message message = (Message) collector.nextResult(2500); - assertNotNull("Message not recieved from remote user", message); + assertNotNull("Message not received from remote user", message); } /** diff --git a/smack-core/src/integration-test/java/org/jivesoftware/smack/test/SmackTestCase.java b/smack-core/src/integration-test/java/org/jivesoftware/smack/test/SmackTestCase.java index 601540abe..ae25506ba 100644 --- a/smack-core/src/integration-test/java/org/jivesoftware/smack/test/SmackTestCase.java +++ b/smack-core/src/integration-test/java/org/jivesoftware/smack/test/SmackTestCase.java @@ -487,7 +487,7 @@ public abstract class SmackTestCase extends TestCase { } /** - * Returns the name of the configuration file related to this test case. By default all + * Returns the name of the configuration file related to this test case. By default,all * the test cases will use the same configuration file. However, it's possible to override the * default configuration by providing a file of the form .xml * (e.g. RosterTest.xml). diff --git a/smack-core/src/main/java/org/jivesoftware/smack/AbstractConnectionListener.java b/smack-core/src/main/java/org/jivesoftware/smack/AbstractConnectionListener.java deleted file mode 100644 index f4a6749b9..000000000 --- a/smack-core/src/main/java/org/jivesoftware/smack/AbstractConnectionListener.java +++ /dev/null @@ -1,51 +0,0 @@ -/** - * - * Copyright 2009 the original author or authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.jivesoftware.smack; - -/** - * The AbstractConnectionListener class provides an empty implementation for all - * methods defined by the {@link ConnectionListener} interface. This is a - * convenience class which should be used in case you do not need to implement - * all methods. - * - * @author Henning Staib - * @deprecated use {@link ConnectionListener} instead. - */ -// TODO: Remove in Smack 4.5. -@Deprecated -public class AbstractConnectionListener implements ConnectionListener { - @Override - public void connected(XMPPConnection connection) { - // do nothing - } - - @Override - public void authenticated(XMPPConnection connection, boolean resumed) { - // do nothing - } - - @Override - public void connectionClosed() { - // do nothing - } - - @Override - public void connectionClosedOnError(Exception e) { - // do nothing - } - -} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java b/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java index 2b2dd5ee8..a3fc2f941 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java @@ -1,6 +1,6 @@ /** * - * Copyright 2009 Jive Software, 2018-2022 Florian Schmaus. + * Copyright 2009 Jive Software, 2018-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. @@ -19,6 +19,7 @@ package org.jivesoftware.smack; import java.io.IOException; import java.io.Reader; import java.io.Writer; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; @@ -128,7 +129,7 @@ import org.jxmpp.stringprep.XmppStringprepException; import org.jxmpp.util.XmppStringUtils; /** - * This abstract class is commonly used as super class for XMPP connection mechanisms like TCP and BOSH. Hence it + * This abstract class is commonly used as super class for XMPP connection mechanisms like TCP and BOSH. Hence, it * provides the methods for connection state management, like {@link #connect()}, {@link #login()} and * {@link #disconnect()} (which are deliberately not provided by the {@link XMPPConnection} interface). *

@@ -387,6 +388,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { * * @param configuration The configuration which is used to establish the connection. */ + @SuppressWarnings("this-escape") protected AbstractXMPPConnection(ConnectionConfiguration configuration) { saslAuthentication = new SASLAuthentication(this, configuration); config = configuration; @@ -1067,6 +1069,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { } @Override + @SuppressWarnings("TypeParameterUnusedInFormals") public I sendIqRequestAndWaitForResponse(IQ request) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { StanzaCollector collector = createStanzaCollectorAndSend(request); @@ -1214,7 +1217,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { } Stanza packet = (Stanza) sendTopLevelStreamElement; - final List listenersToNotify = new LinkedList<>(); + final List listenersToNotify = new ArrayList<>(); synchronized (sendListeners) { for (ListenerWrapper listenerWrapper : sendListeners.values()) { if (listenerWrapper.filterMatches(packet)) { @@ -1242,27 +1245,6 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { }); } - @Deprecated - @Override - public void addStanzaInterceptor(StanzaListener packetInterceptor, - StanzaFilter packetFilter) { - if (packetInterceptor == null) { - throw new NullPointerException("Packet interceptor is null."); - } - InterceptorWrapper interceptorWrapper = new InterceptorWrapper(packetInterceptor, packetFilter); - synchronized (interceptors) { - interceptors.put(packetInterceptor, interceptorWrapper); - } - } - - @Deprecated - @Override - public void removeStanzaInterceptor(StanzaListener packetInterceptor) { - synchronized (interceptors) { - interceptors.remove(packetInterceptor); - } - } - private static , MP extends MessageOrPresence> void addInterceptor( Map, GenericInterceptorWrapper> interceptors, Consumer interceptor, Predicate filter) { @@ -1305,7 +1287,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { private static , MP extends MessageOrPresence> MP fireMessageOrPresenceInterceptors( MP messageOrPresence, Map, GenericInterceptorWrapper> interceptors) { - List> interceptorsToInvoke = new LinkedList<>(); + List> interceptorsToInvoke = new ArrayList<>(); synchronized (interceptors) { for (GenericInterceptorWrapper interceptorWrapper : interceptors.values()) { if (interceptorWrapper.filterMatches(messageOrPresence)) { @@ -1340,7 +1322,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { * @return the, potentially modified stanza, after the interceptors are run. */ private Stanza firePacketInterceptors(Stanza packet) { - List interceptorsToInvoke = new LinkedList<>(); + List interceptorsToInvoke = new ArrayList<>(); synchronized (interceptors) { for (InterceptorWrapper interceptorWrapper : interceptors.values()) { if (interceptorWrapper.filterMatches(packet)) { @@ -1625,7 +1607,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { // First handle the async recv listeners. Note that this code is very similar to what follows a few lines below, // the only difference is that asyncRecvListeners is used here and that the packet listeners are started in // their own thread. - final Collection listenersToNotify = new LinkedList<>(); + final Collection listenersToNotify = new ArrayList<>(); extractMatchingListeners(packet, asyncRecvListeners, listenersToNotify); for (final StanzaListener listener : listenersToNotify) { asyncGoLimited(new Runnable() { @@ -1951,7 +1933,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { // Default implementation does nothing } - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) @Override public F getFeature(QName qname) { return (F) streamFeatures.get(qname); @@ -2196,6 +2178,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { * {@link #maxAsyncRunnables}. Note that we use a {@code LinkedList} in order to avoid space blowups in case the * list ever becomes very big and shrinks again. */ + @SuppressWarnings("JdkObsolete") private final Queue deferredAsyncRunnables = new LinkedList<>(); private int deferredAsyncRunnablesCount; diff --git a/smack-core/src/main/java/org/jivesoftware/smack/AsyncButOrdered.java b/smack-core/src/main/java/org/jivesoftware/smack/AsyncButOrdered.java index 526e3cd2e..5788370ff 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/AsyncButOrdered.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/AsyncButOrdered.java @@ -53,14 +53,14 @@ import java.util.concurrent.Executor; public class AsyncButOrdered { /** - * A map with the currently pending runnables for a given key. Note that this is a weak hash map so we do not have - * to take care of removing the keys ourselfs from the map. + * A map with the currently pending runnables for a given key. Note that this is a weak hash map, so we do not have + * to take care of removing the keys ourselves from the map. */ private final Map> pendingRunnables = new WeakHashMap<>(); /** * A marker map if there is an active thread for the given key. Holds the responsible handler thread if one is - * active, otherwise the key is non-existend in the map. + * active, otherwise the key is non-existent in the map. */ private final Map threadActiveMap = new HashMap<>(); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/ConnectionConfiguration.java b/smack-core/src/main/java/org/jivesoftware/smack/ConnectionConfiguration.java index 9561a158d..e9d8afe45 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/ConnectionConfiguration.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/ConnectionConfiguration.java @@ -1,6 +1,6 @@ /** * - * Copyright 2003-2007 Jive Software, 2017-2022 Florian Schmaus. + * Copyright 2003-2007 Jive Software, 2017-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. @@ -88,7 +88,7 @@ import org.minidns.util.InetAddressUtil; /** * The connection configuration used for XMPP client-to-server connections. A well configured XMPP service will * typically only require you to provide two parameters: The XMPP address, also known as the JID, of the user and the - * password. All other configuration parameters could ideally be determined automatically by Smack. Hence it is often + * password. All other configuration parameters could ideally be determined automatically by Smack. Hence, it is often * enough to call {@link Builder#setXmppAddressAndPassword(CharSequence, String)}. *

* Technically there are typically at least two parameters required: Some kind of credentials for authentication. And @@ -248,7 +248,7 @@ public abstract class ConnectionConfiguration { stanzaIdSourceFactory = builder.stanzaIdSourceFactory; - // If the enabledSaslmechanisms are set, then they must not be empty + // If the enabledSaslMechanisms are set, then they must not be empty assert enabledSaslMechanisms == null || !enabledSaslMechanisms.isEmpty(); } @@ -267,7 +267,7 @@ public abstract class ConnectionConfiguration { context = SSLContext.getInstance("TLS"); } - // TODO: Remove the block below once we removed setKeystorePath(), setKeystoreType(), setCallbackHanlder() and + // TODO: Remove the block below once we removed setKeystorePath(), setKeystoreType(), setCallbackHandler() and // setPKCS11Library() in the builder, and all related fields and the parameters of this function. if (keyManagers == null) { keyManagers = Builder.getKeyManagersFrom(keystoreType, keystorePath, callbackHandler, pkcs11Library); @@ -534,7 +534,7 @@ public abstract class ConnectionConfiguration { /** * Returns the stream language to use when connecting to the server. * - * @return the stream language to use when connecting to the server. + * @return the stream language to use when connecting to the server or null. */ public Locale getLanguage() { return language; @@ -544,19 +544,21 @@ public abstract class ConnectionConfiguration { * Returns the xml:lang string of the stream language to use when connecting to the server. * *

If the developer sets the language to null, this will also return null, leading to - * the removal of the xml:lang tag from the stream. If a Locale("") is configured, this will - * return "", which can be used as an override.

+ * the removal of the xml:lang tag from the stream.

* - * @return the stream language to use when connecting to the server. + * @return the stream language to use when connecting to the server or null. */ public String getXmlLang() { - // TODO: Change to Locale.toLanguageTag() once Smack's minimum Android API level is 21 or higher. - // This will need a workaround for new Locale("").getLanguageTag() returning "und". Expected - // behavior of this function: - // - returns null if language is null - // - returns "" if language.getLanguage() returns the empty string - // - returns language.toLanguageTag() otherwise - return language != null ? language.toString().replace("_", "-") : null; + if (language == null) { + return null; + } + + String languageTag = language.toLanguageTag(); + if (languageTag.equals("und")) { + return null; + } + + return languageTag; } /** @@ -583,7 +585,7 @@ public abstract class ConnectionConfiguration { * Returns true if the connection is going to use stream compression. Stream compression * will be requested after TLS was established (if TLS was enabled) and only if the server * offered stream compression. With stream compression network traffic can be reduced - * up to 90%. By default compression is disabled. + * up to 90%. By default,compression is disabled. * * @return true if the connection is going to use stream compression. */ @@ -592,7 +594,7 @@ public abstract class ConnectionConfiguration { } /** - * Check if the given SASL mechansism is enabled in this connection configuration. + * Check if the given SASL mechanism is enabled in this connection configuration. * * @param saslMechanism TODO javadoc me please * @return true if the given SASL mechanism is enabled, false otherwise. @@ -607,7 +609,7 @@ public abstract class ConnectionConfiguration { /** * Return the explicitly enabled SASL mechanisms. May return null if no SASL mechanisms where - * explicitly enabled, i.e. all SALS mechanisms supported and announced by the service will be considered. + * explicitly enabled, i.e. all SASL mechanisms supported and announced by the service will be considered. * * @return the enabled SASL mechanisms or null. */ @@ -670,6 +672,7 @@ public abstract class ConnectionConfiguration { private boolean compressionEnabled = false; private StanzaIdSourceFactory stanzaIdSourceFactory = new StandardStanzaIdSource.Factory(); + @SuppressWarnings("this-escape") protected Builder() { if (SmackConfiguration.DEBUG) { enableDefaultDebugger(); @@ -857,22 +860,6 @@ public abstract class ConnectionConfiguration { return getThis(); } - /** - * Set the host to connect to by either its fully qualified domain name (FQDN) or its IP. - * - * @param fqdnOrIp a CharSequence either representing the FQDN or the IP of the host. - * @return a reference to this builder. - * @see #setHost(DnsName) - * @see #setHostAddress(InetAddress) - * @since 4.3.2 - * @deprecated use {@link #setHost(CharSequence)} instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public B setHostAddressByNameOrIp(CharSequence fqdnOrIp) { - return setHost(fqdnOrIp); - } - public B setPort(int port) { if (port < 0 || port > 65535) { throw new IllegalArgumentException( @@ -1021,25 +1008,6 @@ public abstract class ConnectionConfiguration { return getThis(); } - /** - * Sets a custom SSLContext for creating SSL sockets. - *

- * For more information on how to create a SSLContext see Java Secure Socket Extension (JSEE) Reference Guide: Creating Your Own X509TrustManager - * - * @param context the custom SSLContext for new sockets. - * @return a reference to this builder. - * @deprecated use {@link #setSslContextFactory(SslContextFactory)} instead}. - */ - // TODO: Remove in Smack 4.5. - @Deprecated - public B setCustomSSLContext(SSLContext context) { - return setSslContextFactory(() -> { - return context; - }); - } - /** * Sets a custom SSLContext for creating SSL sockets. *

@@ -1090,8 +1058,7 @@ public abstract class ConnectionConfiguration { } /** - * Sets if an initial available presence will be sent to the server. By default - * an available presence will be sent to the server indicating that this presence + * Sets if an initial available presence will be sent to the server. By default, * an available presence will be sent to the server indicating that this presence * is not online and available to receive messages. If you want to log in without * being 'noticed' then pass a false value. * @@ -1187,7 +1154,9 @@ public abstract class ConnectionConfiguration { if (!SASLAuthentication.isSaslMechanismRegistered(SASLMechanism.EXTERNAL)) { throw new IllegalArgumentException("SASL " + SASLMechanism.EXTERNAL + " is not registered"); } - setCustomSSLContext(sslContext); + setSslContextFactory(() -> { + return sslContext; + }); throwIfEnabledSaslMechanismsSet(); allowEmptyOrNullUsernames(); @@ -1266,7 +1235,7 @@ public abstract class ConnectionConfiguration { * Sets if the connection is going to use compression (default false). * * Compression is only activated if the server offers compression. With compression network - * traffic can be reduced up to 90%. By default compression is disabled. + * traffic can be reduced up to 90%. By default,compression is disabled. * * @param compressionEnabled if the connection is going to use compression on the HTTP level. * @return a reference to this object. @@ -1324,7 +1293,7 @@ public abstract class ConnectionConfiguration { } else { InputStream stream = TLSUtils.getDefaultTruststoreStreamIfPossible(); try { - // Note that PKCS12 keystores need a password one some Java platforms. Hence we try the famous + // Note that PKCS12 keystores need a password one some Java platforms. Hence, we try the famous // 'changeit' here. See https://bugs.openjdk.java.net/browse/JDK-8194702 char[] password = "changeit".toCharArray(); try { diff --git a/smack-core/src/main/java/org/jivesoftware/smack/SASLAuthentication.java b/smack-core/src/main/java/org/jivesoftware/smack/SASLAuthentication.java index 4685bf4af..7d545bb45 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/SASLAuthentication.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/SASLAuthentication.java @@ -53,7 +53,7 @@ import org.jxmpp.jid.EntityBareJid; * *

Once TLS has been negotiated (i.e. the connection has been secured) it is possible to * register with the server or authenticate using SASL. If the - * server supports SASL then Smack will try to authenticate using SASL..

+ * server supports SASL then Smack will try to authenticate using SASL.

* *

The server may support many SASL mechanisms to use for authenticating. Out of the box * Smack provides several SASL mechanisms, but it is possible to register new SASL Mechanisms. Use diff --git a/smack-core/src/main/java/org/jivesoftware/smack/ScheduledAction.java b/smack-core/src/main/java/org/jivesoftware/smack/ScheduledAction.java index da0377fab..def612a19 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/ScheduledAction.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/ScheduledAction.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018 Florian Schmaus + * Copyright 2018-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. @@ -50,11 +50,13 @@ public class ScheduledAction implements Delayed { return smackReactor.cancel(this); } + @SuppressWarnings("JavaUtilDate") public boolean isDue() { Date now = new Date(); return now.after(releaseTime); } + @SuppressWarnings("JavaUtilDate") public long getTimeToDueMillis() { long now = System.currentTimeMillis(); return releaseTime.getTime() - now; diff --git a/smack-core/src/main/java/org/jivesoftware/smack/Smack.java b/smack-core/src/main/java/org/jivesoftware/smack/Smack.java index 6608bba4b..daa7a18b4 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/Smack.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/Smack.java @@ -30,7 +30,7 @@ public class Smack { public static final String SMACK_PACKAGE = SMACK_ORG + ".smack"; /** - * Returns the Smack version information, eg "1.3.0". + * Returns the Smack version information, e.g."1.3.0". * * @return the Smack version information. */ diff --git a/smack-core/src/main/java/org/jivesoftware/smack/SmackConfiguration.java b/smack-core/src/main/java/org/jivesoftware/smack/SmackConfiguration.java index f3316c8ed..ef1101cff 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/SmackConfiguration.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/SmackConfiguration.java @@ -102,7 +102,7 @@ public final class SmackConfiguration { private static HostnameVerifier defaultHostnameVerififer; /** - * Returns the Smack version information, eg "1.3.0". + * Returns the Smack version information, e.g."1.3.0". * * @return the Smack version information. * @deprecated use {@link Smack#getVersion()} instead. diff --git a/smack-core/src/main/java/org/jivesoftware/smack/SmackInitialization.java b/smack-core/src/main/java/org/jivesoftware/smack/SmackInitialization.java index 2a5b79c6a..82098094f 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/SmackInitialization.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/SmackInitialization.java @@ -1,6 +1,6 @@ /** * - * Copyright 2003-2007 Jive Software, 2014-2020 Florian Schmaus + * Copyright 2003-2007 Jive Software, 2014-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. @@ -63,7 +63,7 @@ public final class SmackInitialization { private static final Logger LOGGER = Logger.getLogger(SmackInitialization.class.getName()); - /** + /* * Loads the configuration from the smack-config.xml and system properties file. *

* So far this means that: diff --git a/smack-core/src/main/java/org/jivesoftware/smack/SmackReactor.java b/smack-core/src/main/java/org/jivesoftware/smack/SmackReactor.java index 67e68f1fd..8fc59ab3a 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/SmackReactor.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/SmackReactor.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018-2020 Florian Schmaus + * Copyright 2018-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. @@ -144,6 +144,7 @@ public class SmackReactor { } } + @SuppressWarnings("JavaUtilDate") ScheduledAction schedule(Runnable runnable, long delay, TimeUnit unit, ScheduledAction.Kind scheduledActionKind) { long releaseTimeEpoch = System.currentTimeMillis() + unit.toMillis(delay); Date releaseTimeDate = new Date(releaseTimeEpoch); @@ -276,8 +277,7 @@ public class SmackReactor { setInterestOpsCancelledKeySafe(selectionKey, 0); } - selectedKeys = new ArrayList<>(selectedKeySet.size()); - selectedKeys.addAll(selectedKeySet); + selectedKeys = new ArrayList<>(selectedKeySet); selectedKeySet.clear(); } @@ -327,6 +327,12 @@ public class SmackReactor { int currentReactorThreadCount = reactorThreads.size(); int myKeyCount = pendingSelectionKeysSize / currentReactorThreadCount; + // The division could result in myKeyCount being zero, even though there are pending selection keys. + // Therefore, ensure that this thread tries to get at least one pending selection key by invoking poll(). + // Otherwise, it could happen that we end up in a busy loop, where myKeyCount is zero and this thread invokes + // selector.wakeup() below because pendingSelectionsKeys is not empty, but the woken up reactor thread wil + // end up with myKeyCount being zero again, restarting the busy-loop cycle. + if (myKeyCount == 0) myKeyCount = 1; Collection 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/StanzaCollector.java b/smack-core/src/main/java/org/jivesoftware/smack/StanzaCollector.java index 2ceaaf19d..dfa0ec7d2 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/StanzaCollector.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/StanzaCollector.java @@ -1,6 +1,6 @@ /** * - * Copyright 2003-2007 Jive Software, 2016-2019 Florian Schmaus. + * Copyright 2003-2007 Jive Software, 2016-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. @@ -117,7 +117,7 @@ public final class StanzaCollector implements AutoCloseable { * @return the next stanza result, or null if there are no more * results. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) public synchronized

P pollResult() { return (P) resultQueue.poll(); } @@ -134,6 +134,7 @@ public final class StanzaCollector implements AutoCloseable { * @return the next available packet. * @throws XMPPErrorException in case an error response. */ + @SuppressWarnings("TypeParameterUnusedInFormals") public

P pollResultOrThrow() throws XMPPErrorException { P result = pollResult(); if (result != null) { @@ -150,7 +151,7 @@ public final class StanzaCollector implements AutoCloseable { * @return the next available packet. * @throws InterruptedException if the calling thread was interrupted. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) // TODO: Consider removing this method as it is hardly ever useful. public synchronized

P nextResultBlockForever() throws InterruptedException { throwIfCancelled(); @@ -175,6 +176,7 @@ public final class StanzaCollector implements AutoCloseable { * @return the next available packet. * @throws InterruptedException if the calling thread was interrupted. */ + @SuppressWarnings("TypeParameterUnusedInFormals") public

P nextResult() throws InterruptedException { return nextResult(connection.getReplyTimeout()); } @@ -191,7 +193,7 @@ public final class StanzaCollector implements AutoCloseable { * @return the next available stanza or null on timeout or connection error. * @throws InterruptedException if the calling thread was interrupted. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) public

P nextResult(long timeout) throws InterruptedException { throwIfCancelled(); P res = null; @@ -223,6 +225,7 @@ public final class StanzaCollector implements AutoCloseable { * @throws NotConnectedException if the XMPP connection is not connected. * @see #nextResultOrThrow(long) */ + @SuppressWarnings("TypeParameterUnusedInFormals") public

P nextResultOrThrow() throws NoResponseException, XMPPErrorException, InterruptedException, NotConnectedException { return nextResultOrThrow(connection.getReplyTimeout()); @@ -263,6 +266,7 @@ public final class StanzaCollector implements AutoCloseable { * @throws InterruptedException if the calling thread was interrupted. * @throws NotConnectedException if there was no response and the connection got disconnected. */ + @SuppressWarnings("TypeParameterUnusedInFormals") public

P nextResultOrThrow(long timeout) throws NoResponseException, XMPPErrorException, InterruptedException, NotConnectedException { P result; diff --git a/smack-core/src/main/java/org/jivesoftware/smack/StanzaListener.java b/smack-core/src/main/java/org/jivesoftware/smack/StanzaListener.java index 52b3168c8..786c5a8b0 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/StanzaListener.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/StanzaListener.java @@ -27,12 +27,6 @@ import org.jivesoftware.smack.packet.Stanza; * the {@link #processStanza(Stanza)} method will be called. This is the * opposite approach to the functionality provided by a {@link StanzaCollector} * which lets you block while waiting for results. - *

- * Additionally you are able to intercept Packets that are going to be send and - * make modifications to them. You can register a PacketListener as interceptor - * by using {@link XMPPConnection#addStanzaInterceptor(StanzaListener, - * org.jivesoftware.smack.filter.StanzaFilter)} - *

* * @see XMPPConnection#addAsyncStanzaListener(StanzaListener, org.jivesoftware.smack.filter.StanzaFilter) * @author Matt Tucker 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..45cd8328f 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 + * 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 @@ -241,7 +242,7 @@ public interface XMPPConnection { *

* * @param stanza the stanza to send. - * @return {@code true} if the stanza was successfully scheduled to be send, {@code false} otherwise. + * @return {@code true} if the stanza was successfully scheduled to be sent, {@code false} otherwise. * @throws NotConnectedException if the connection is not connected. * @since 4.4.0 * @deprecated use {@link #sendStanzaNonBlocking(Stanza)} instead. @@ -264,7 +265,7 @@ public interface XMPPConnection { * @param stanza the stanza to send. * @param timeout how long to wait before giving up, in units of {@code unit}. * @param unit a {@code TimeUnit} determining how to interpret the {@code timeout} parameter. - * @return {@code true} if the stanza was successfully scheduled to be send, {@code false} otherwise. + * @return {@code true} if the stanza was successfully scheduled to be sent, {@code false} otherwise. * @throws NotConnectedException if the connection is not connected. * @throws InterruptedException if the calling thread was interrupted. * @since 4.4.0 @@ -317,6 +318,7 @@ public interface XMPPConnection { * @throws InterruptedException if the calling thread was interrupted. * @since 4.3 */ + @SuppressWarnings("TypeParameterUnusedInFormals") I sendIqRequestAndWaitForResponse(IQ request) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException; @@ -409,7 +411,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. *

@@ -422,7 +424,6 @@ public interface XMPPConnection { * * @param stanzaListener the stanza listener to notify of new received stanzas. * @param stanzaFilter the stanza filter to use. - * @see #addStanzaInterceptor(StanzaListener, StanzaFilter) * @since 4.1 */ void addSyncStanzaListener(StanzaListener stanzaListener, StanzaFilter stanzaFilter); @@ -448,7 +449,6 @@ public interface XMPPConnection { * * @param stanzaListener the stanza listener to notify of new received stanzas. * @param stanzaFilter the stanza filter to use. - * @see #addStanzaInterceptor(StanzaListener, StanzaFilter) * @since 4.1 */ void addAsyncStanzaListener(StanzaListener stanzaListener, StanzaFilter stanzaFilter); @@ -482,34 +482,6 @@ public interface XMPPConnection { */ void removeStanzaSendingListener(StanzaListener stanzaListener); - /** - * Registers a stanza interceptor with this connection. The interceptor will be - * invoked every time a stanza is about to be sent by this connection. Interceptors - * may modify the stanza to be sent. A stanza filter determines which stanzas - * will be delivered to the interceptor. - * - *

- * NOTE: For a similar functionality on incoming stanzas, see {@link #addAsyncStanzaListener(StanzaListener, StanzaFilter)}. - *

- * - * @param stanzaInterceptor the stanza interceptor to notify of stanzas about to be sent. - * @param stanzaFilter the stanza filter to use. - * @deprecated use {@link #addMessageInterceptor(Consumer, Predicate)} or {@link #addPresenceInterceptor(Consumer, Predicate)} instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - void addStanzaInterceptor(StanzaListener stanzaInterceptor, StanzaFilter stanzaFilter); - - /** - * Removes a stanza interceptor. - * - * @param stanzaInterceptor the stanza interceptor to remove. - * @deprecated use {@link #removeMessageInterceptor(Consumer)} or {@link #removePresenceInterceptor(Consumer)} instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - void removeStanzaInterceptor(StanzaListener stanzaInterceptor); - /** * Registers a stanza interceptor with this connection. The interceptor will be * invoked every time a stanza is about to be sent by this connection. Interceptors @@ -610,23 +582,6 @@ public interface XMPPConnection { */ FromMode getFromMode(); - /** - * Get the feature stanza extensions for a given stream feature of the - * server, or null if the server doesn't support that feature. - * - * @param {@link ExtensionElement} type of the feature. - * @param element TODO javadoc me please - * @param namespace TODO javadoc me please - * @return a stanza extensions of the feature or null - * @deprecated use {@link #getFeature(Class)} instead. - */ - // TODO: Remove in Smack 4.5. - @Deprecated - default F getFeature(String element, String namespace) { - QName qname = new QName(namespace, element); - return getFeature(qname); - } - /** * Get the feature stanza extensions for a given stream feature of the * server, or null if the server doesn't support that feature. @@ -636,6 +591,7 @@ public interface XMPPConnection { * @return a stanza extensions of the feature or null * @since 4.4 */ + @SuppressWarnings("TypeParameterUnusedInFormals") F getFeature(QName qname); /** diff --git a/smack-core/src/main/java/org/jivesoftware/smack/XmppInputOutputFilter.java b/smack-core/src/main/java/org/jivesoftware/smack/XmppInputOutputFilter.java index 97fdff246..4f0c68b0a 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/XmppInputOutputFilter.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/XmppInputOutputFilter.java @@ -57,8 +57,8 @@ public interface XmppInputOutputFilter { } /** - * The returned {@link ByteBuffer} is going to get fliped by the caller. The callee must not flip the buffer. - * @param inputData the data this methods needs to process. + * The returned {@link ByteBuffer} is going to get flipped by the caller. The callee must not flip the buffer. + * @param inputData the data this method needs to process. * @return a {@link ByteBuffer} or {@code null} if no data could be produced. * @throws IOException in case an I/O exception occurs. */ diff --git a/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnection.java b/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnection.java index b24b00704..ca3de8190 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnection.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnection.java @@ -301,7 +301,7 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne currentStateVertex = StateDescriptorGraph.convertToStateGraph(initialStateDescriptorVertex, connectionInternal); } - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) public > CM getConnectionModuleFor( Class descriptorClass) { return (CM) connectionModules.get(descriptorClass); @@ -390,7 +390,7 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne // Ignore successorStateVertex if the only way to the final state is via the initial state. This happens // typically if we are in the ConnectedButUnauthenticated state on the way to ResourceboundAndAuthenticated, - // where we do not want to walk via InstantShutdown/Shtudown in a cycle over the initial state towards this + // where we do not want to walk via InstantShutdown/Shutdown in a cycle over the initial state towards this // state. if (walkStateGraphContext.wouldCauseCycle(successorStateVertex)) { // Ignore this successor. @@ -658,6 +658,7 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne updateOutgoingStreamXmlEnvironmentOnStreamOpen(streamOpen); } + @SuppressWarnings("this-escape") public static class DisconnectedStateDescriptor extends StateDescriptor { protected DisconnectedStateDescriptor() { super(DisconnectedState.class, StateDescriptor.Property.finalState); @@ -666,7 +667,8 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne } private final class DisconnectedState extends State { - + // Invoked via reflection. + @SuppressWarnings("UnusedMethod") private DisconnectedState(StateDescriptor stateDescriptor, ModularXmppClientToServerConnectionInternal connectionInternal) { super(stateDescriptor, connectionInternal); @@ -707,6 +709,8 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne private final class LookupRemoteConnectionEndpointsState extends State { boolean outgoingElementsQueueWasShutdown; + // Invoked via reflection. + @SuppressWarnings("UnusedMethod") private LookupRemoteConnectionEndpointsState(StateDescriptor stateDescriptor, ModularXmppClientToServerConnectionInternal connectionInternal) { super(stateDescriptor, connectionInternal); @@ -817,6 +821,8 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne } private final class ConnectedButUnauthenticatedState extends State { + // Invoked via reflection. + @SuppressWarnings("UnusedMethod") private ConnectedButUnauthenticatedState(StateDescriptor stateDescriptor, ModularXmppClientToServerConnectionInternal connectionInternal) { super(stateDescriptor, connectionInternal); @@ -849,6 +855,8 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne } private final class SaslAuthenticationState extends State { + // Invoked via reflection. + @SuppressWarnings("UnusedMethod") private SaslAuthenticationState(StateDescriptor stateDescriptor, ModularXmppClientToServerConnectionInternal connectionInternal) { super(stateDescriptor, connectionInternal); @@ -892,6 +900,8 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne } public static final class ResourceBindingStateDescriptor extends StateDescriptor { + // Invoked via reflection. + @SuppressWarnings("UnusedMethod") private ResourceBindingStateDescriptor() { super(ResourceBindingState.class, "RFC 6120 § 7"); addSuccessor(AuthenticatedAndResourceBoundStateDescriptor.class); @@ -899,6 +909,8 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne } private final class ResourceBindingState extends State { + // Invoked via reflection. + @SuppressWarnings("UnusedMethod") private ResourceBindingState(StateDescriptor stateDescriptor, ModularXmppClientToServerConnectionInternal connectionInternal) { super(stateDescriptor, connectionInternal); @@ -954,6 +966,8 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne } private final class AuthenticatedAndResourceBoundState extends State { + // Invoked via reflection. + @SuppressWarnings("UnusedMethod") private AuthenticatedAndResourceBoundState(StateDescriptor stateDescriptor, ModularXmppClientToServerConnectionInternal connectionInternal) { super(stateDescriptor, connectionInternal); @@ -994,6 +1008,8 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne } private final class ShutdownState extends State { + // Invoked via reflection. + @SuppressWarnings("UnusedMethod") private ShutdownState(StateDescriptor stateDescriptor, ModularXmppClientToServerConnectionInternal connectionInternal) { super(stateDescriptor, connectionInternal); @@ -1056,6 +1072,8 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne } private static final class InstantShutdownState extends NoOpState { + // Invoked via reflection. + @SuppressWarnings("UnusedMethod") private InstantShutdownState(ModularXmppClientToServerConnection connection, StateDescriptor stateDescriptor, ModularXmppClientToServerConnectionInternal connectionInternal) { super(connection, stateDescriptor, connectionInternal); @@ -1077,6 +1095,8 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne } private final class CloseConnectionState extends State { + // Invoked via reflection. + @SuppressWarnings("UnusedMethod") private CloseConnectionState(StateDescriptor stateDescriptor, ModularXmppClientToServerConnectionInternal connectionInternal) { super(stateDescriptor, connectionInternal); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/compression/CompressionModuleDescriptor.java b/smack-core/src/main/java/org/jivesoftware/smack/compression/CompressionModuleDescriptor.java index 3cf43ee79..4fa0e6045 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/compression/CompressionModuleDescriptor.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/compression/CompressionModuleDescriptor.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018-2020 Florian Schmaus + * Copyright 2018-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. @@ -41,6 +41,8 @@ public class CompressionModuleDescriptor extends ModularXmppClientToServerConnec public static final class Builder extends ModularXmppClientToServerConnectionModuleDescriptor.Builder { + // Invoked via reflection. + @SuppressWarnings("UnusedMethod") private Builder(ModularXmppClientToServerConnectionConfiguration.Builder connectionConfigurationBuilder) { super(connectionConfigurationBuilder); } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/compression/zlib/ZlibXmppCompressionFactory.java b/smack-core/src/main/java/org/jivesoftware/smack/compression/zlib/ZlibXmppCompressionFactory.java index 096147931..d851b0c44 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/compression/zlib/ZlibXmppCompressionFactory.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/compression/zlib/ZlibXmppCompressionFactory.java @@ -143,7 +143,9 @@ public final class ZlibXmppCompressionFactory extends XmppCompressionFactory { int bytesWritten = compressor.deflate(buffer, initialOutputBufferPosition, length, flushMode); int newOutputBufferPosition = initialOutputBufferPosition + bytesWritten; - outputBuffer.position(newOutputBufferPosition); + // Workaround for Android API not matching Java >=9 API. + // See https://issuetracker.google.com/issues/369219141 + ((java.nio.Buffer) outputBuffer).position(newOutputBufferPosition); totalBytesWritten += bytesWritten; @@ -156,7 +158,9 @@ public final class ZlibXmppCompressionFactory extends XmppCompressionFactory { increasedBufferSize = MINIMUM_OUTPUT_BUFFER_INCREASE; } ByteBuffer newCurrentOutputBuffer = ByteBuffer.allocate(increasedBufferSize); - outputBuffer.flip(); + // Workaround for Android API not matching Java >=9 API. + // See https://issuetracker.google.com/issues/369219141 + ((java.nio.Buffer) outputBuffer).flip(); newCurrentOutputBuffer.put(outputBuffer); outputBuffer = newCurrentOutputBuffer; } @@ -202,7 +206,9 @@ public final class ZlibXmppCompressionFactory extends XmppCompressionFactory { throw new IOException(e); } - outputBuffer.position(inflateOutputBufferOffset + bytesInflated); + // Workaround for Android API not matching Java >=9 API. + // See https://issuetracker.google.com/issues/369219141 + ((java.nio.Buffer) outputBuffer).position(inflateOutputBufferOffset + bytesInflated); decompressorOutBytes += bytesInflated; @@ -212,7 +218,9 @@ public final class ZlibXmppCompressionFactory extends XmppCompressionFactory { int increasedBufferSize = outputBuffer.capacity() * 2; ByteBuffer increasedOutputBuffer = ByteBuffer.allocate(increasedBufferSize); - outputBuffer.flip(); + // Workaround for Android API not matching Java >=9 API. + // See https://issuetracker.google.com/issues/369219141 + ((java.nio.Buffer) outputBuffer).flip(); increasedOutputBuffer.put(outputBuffer); outputBuffer = increasedOutputBuffer; } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/datatypes/UInt16.java b/smack-core/src/main/java/org/jivesoftware/smack/datatypes/UInt16.java index 271de8cbc..8df4cd2f4 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/datatypes/UInt16.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/datatypes/UInt16.java @@ -19,7 +19,7 @@ package org.jivesoftware.smack.datatypes; import org.jivesoftware.smack.util.NumberUtil; /** - * A number representing an unsigned 16-bit integer. Can be used for values with the XML schema type "xs:unsingedShort". + * A number representing an unsigned 16-bit integer. Can be used for values with the XML schema type "xs:unsignedShort". */ public final class UInt16 extends Scalar implements Comparable { 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..95c339a5d 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 @@ -28,19 +28,20 @@ import org.jivesoftware.smack.util.ExceptionUtil; * even block the thread since only one thread may print at a time. *

* It is possible to not only print the raw sent and received stanzas but also the interpreted - * packets by Smack. By default interpreted packets won't be printed. To enable this feature + * packets by Smack. By default,interpreted packets won't be printed. To enable this feature * just change the printInterpreted static variable to true. *

* * @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); } + @SuppressWarnings("JavaUtilDate") @Override protected void log(String logMessage) { String formatedDate; diff --git a/smack-core/src/main/java/org/jivesoftware/smack/debugger/JulDebugger.java b/smack-core/src/main/java/org/jivesoftware/smack/debugger/JulDebugger.java index db0772271..015bb813b 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/debugger/JulDebugger.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/debugger/JulDebugger.java @@ -27,7 +27,7 @@ import org.jivesoftware.smack.XMPPConnection; * even block the thread since only one thread may print at a time. *

* It is possible to not only print the raw sent and received stanzas but also the interpreted - * packets by Smack. By default interpreted packets won't be printed. To enable this feature + * packets by Smack. By default,interpreted packets won't be printed. To enable this feature * just change the printInterpreted static variable to true. *

* diff --git a/smack-core/src/main/java/org/jivesoftware/smack/debugger/SmackDebugger.java b/smack-core/src/main/java/org/jivesoftware/smack/debugger/SmackDebugger.java index 82ca10b62..4f936b72d 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/debugger/SmackDebugger.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/debugger/SmackDebugger.java @@ -57,7 +57,7 @@ public abstract class SmackDebugger { * * @param user the user@host/resource that has just logged in */ - // TODO: Should be replaced with a connection listener authenticed(). + // TODO: Should be replaced with a connection listener authenticated(). public abstract void userHasLogged(EntityFullJid user); /** diff --git a/smack-core/src/main/java/org/jivesoftware/smack/filter/NotFilter.java b/smack-core/src/main/java/org/jivesoftware/smack/filter/NotFilter.java index 006b5f4f9..5589b2109 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/filter/NotFilter.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/filter/NotFilter.java @@ -1,6 +1,6 @@ /** * - * Copyright 2003-2007 Jive Software. + * Copyright 2003-2007 Jive Software, 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. @@ -17,6 +17,7 @@ package org.jivesoftware.smack.filter; +import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.util.Objects; @@ -43,4 +44,9 @@ public class NotFilter implements StanzaFilter { public boolean accept(Stanza packet) { return !filter.accept(packet); } + + public static NotFilter of(Class extensionElementClass) { + ExtensionElementFilter extensionElementFilter = new ExtensionElementFilter<>(extensionElementClass); + return new NotFilter(extensionElementFilter); + } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/filter/StanzaIdFilter.java b/smack-core/src/main/java/org/jivesoftware/smack/filter/StanzaIdFilter.java index c0d507e50..8e5a5cc2c 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/filter/StanzaIdFilter.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/filter/StanzaIdFilter.java @@ -18,6 +18,7 @@ package org.jivesoftware.smack.filter; import org.jivesoftware.smack.packet.Stanza; +import org.jivesoftware.smack.packet.StanzaView; import org.jivesoftware.smack.util.StringUtils; /** @@ -34,7 +35,7 @@ public class StanzaIdFilter implements StanzaFilter { * * @param stanza the stanza which the ID is taken from. */ - public StanzaIdFilter(Stanza stanza) { + public StanzaIdFilter(StanzaView stanza) { this(stanza.getStanzaId()); } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/filter/package-info.java b/smack-core/src/main/java/org/jivesoftware/smack/filter/package-info.java index 01f2e0461..c130b93ff 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/filter/package-info.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/filter/package-info.java @@ -23,7 +23,7 @@ *
  • {@link StanzaIdFilter}: filters for stanzas with a particular stanza ID
  • *
  • {@link ToMatchesFilter}: filters for stanzas that are sent to a particular address
  • *
  • {@link FromMatchesFilter}: filters for stanzas that are sent from a particular address
  • - *
  • {@link ExtensionElementFilter}: filters for stanzas that have a particular stanza exentsion element
  • + *
  • {@link ExtensionElementFilter}: filters for stanzas that have a particular stanza extension element
  • *
  • {@link AndFilter}: implements the logical AND operation over two or more filters
  • *
  • {@link OrFilter}: implements the logical OR operation over two or more filters
  • *
  • {@link NotFilter}: implements the logical NOT operation on a filter
  • diff --git a/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateDescriptor.java b/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateDescriptor.java index 8f399480e..29c298c76 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateDescriptor.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateDescriptor.java @@ -118,7 +118,7 @@ public abstract class StateDescriptor { if (stateClassConstructor != null) { stateClassConstructor.setAccessible(true); } else { - // TODO: Add validation check that if stateClassConstructor is 'null' the cosntructState() method is overriden. + // TODO: Add validation check that if stateClassConstructor is 'null' the constructState() method is overridden. } String className = getClass().getSimpleName(); @@ -155,7 +155,7 @@ public abstract class StateDescriptor { clazz = Class.forName(clazzName); } catch (ClassNotFoundException e) { // The state descriptor class is not in classpath, which probably means that the smack module is not loaded - // into the classpath. Hence we can silently ignore that. + // into the classpath. Hence, we can silently ignore that. LOGGER.log(Level.FINEST, "Ignoring unknown state descriptor '" + clazzName + "'", e); return; } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateDescriptorGraph.java b/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateDescriptorGraph.java index 41bb29f59..45d23bb36 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateDescriptorGraph.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateDescriptorGraph.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018-2021 Florian Schmaus + * Copyright 2018-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. @@ -215,7 +215,7 @@ public class StateDescriptorGraph { inferredForwardEdges.put(predecessor, backwardsEdge); } } - // Ensure that the intial node has their successors inferred. + // Ensure that the initial node has their successors inferred. for (Class inferredSuccessorOfInitialStateDescriptor : inferredForwardEdges.getAll(initialStatedescriptorClass)) { initialNode.getElement().addSuccessor(inferredSuccessorOfInitialStateDescriptor); } @@ -368,7 +368,7 @@ public class StateDescriptorGraph { } } - public static void stateDescriptorGraphToDot(Collection> vertexes, + public static void stateDescriptorGraphToDot(Collection> vertexes, PrintWriter dotOut, boolean breakStateName) { dotOut.append("digraph {\n"); dfs(vertexes, diff --git a/smack-core/src/main/java/org/jivesoftware/smack/initializer/UrlInitializer.java b/smack-core/src/main/java/org/jivesoftware/smack/initializer/UrlInitializer.java index ec7b047e7..33ced4f39 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/initializer/UrlInitializer.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/initializer/UrlInitializer.java @@ -1,6 +1,6 @@ /** * - * Copyright 2014-2018 Florian Schmaus + * Copyright 2014-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. @@ -18,7 +18,7 @@ package org.jivesoftware.smack.initializer; import java.io.InputStream; import java.net.URI; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; @@ -42,7 +42,7 @@ public abstract class UrlInitializer implements SmackInitializer { public List initialize() { InputStream is = null; final ClassLoader classLoader = this.getClass().getClassLoader(); - final List exceptions = new LinkedList(); + final List exceptions = new ArrayList(); final String providerUriString = getProvidersUri(); if (providerUriString != null) { try { diff --git a/smack-core/src/main/java/org/jivesoftware/smack/isr/InstantStreamResumptionModuleDescriptor.java b/smack-core/src/main/java/org/jivesoftware/smack/isr/InstantStreamResumptionModuleDescriptor.java index d26703eec..3e8171076 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/isr/InstantStreamResumptionModuleDescriptor.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/isr/InstantStreamResumptionModuleDescriptor.java @@ -41,6 +41,8 @@ public class InstantStreamResumptionModuleDescriptor extends ModularXmppClientTo public static final class Builder extends ModularXmppClientToServerConnectionModuleDescriptor.Builder { + // Unfinished implementation. + @SuppressWarnings("UnusedMethod") private Builder(ModularXmppClientToServerConnectionConfiguration.Builder connectionConfigurationBuilder) { super(connectionConfigurationBuilder); } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/AbstractError.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/AbstractError.java index 147b4f031..50067fc14 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/AbstractError.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/AbstractError.java @@ -1,6 +1,6 @@ /** * - * Copyright 2014-2021 Florian Schmaus + * Copyright 2014-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. @@ -108,6 +108,7 @@ public class AbstractError { * @param type of the ExtensionElement. * @return the extension, or null if it doesn't exist. */ + @SuppressWarnings("TypeParameterUnusedInFormals") public PE getExtension(String elementName, String namespace) { return PacketUtil.extensionElementFrom(extensions, elementName, namespace); } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/AbstractTextElement.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/AbstractTextElement.java index 00e82d704..2f475fa06 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/AbstractTextElement.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/AbstractTextElement.java @@ -1,6 +1,6 @@ /** * - * Copyright © 2017-2019 Florian Schmaus + * Copyright © 2017-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. @@ -54,15 +54,4 @@ public abstract class AbstractTextElement implements ExtensionElement { return lang; } - /** - * Deprecated. - * - * @return deprecated - * @deprecated use {@link #getLanguage()} instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public final String getLang() { - return lang; - } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/EmptyResultIQ.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/EmptyResultIQ.java index a4d977ac3..246e9de06 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/EmptyResultIQ.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/EmptyResultIQ.java @@ -1,6 +1,6 @@ /** * - * Copyright © 2014-2023 Florian Schmaus + * Copyright © 2014-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. @@ -23,6 +23,7 @@ public class EmptyResultIQ extends IQ { } // TODO: Deprecate when stanza builder and parsing logic is ready. + @SuppressWarnings("this-escape") public EmptyResultIQ() { super((String) null, null); setType(IQ.Type.result); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/ExtensionElement.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/ExtensionElement.java index 5a7f00650..5f4ed1b6e 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/ExtensionElement.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/ExtensionElement.java @@ -19,7 +19,7 @@ package org.jivesoftware.smack.packet; /** * Interface to represent XMPP extension elements. Unlike {@link XmlElement}, every non-abstract class that implements * {@link ExtensionElement} must have a static final QNAME member of the type {@link javax.xml.namespace.QName}. This - * allows type-safe functions like {@link StanzaView#getExtension(Class)}. Hence this is a marker interface. + * allows type-safe functions like {@link StanzaView#getExtension(Class)}. Hence, this is a marker interface. *

    * Use this class when implementing new extension elements when possible. This means that every instance of your * implemented class must represent an XML element of the same qualified name. diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/IQ.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/IQ.java index 1881f4c23..98f9880f3 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/IQ.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/IQ.java @@ -88,14 +88,14 @@ public abstract class IQ extends Stanza implements IqView { } @Override - public Type getType() { + public final Type getType() { return type; } /** * Sets the type of the IQ packet. *

    - * Since the type of an IQ must present, an IllegalArgmentException will be thrown when type is + * Since the type of an IQ must present, an IllegalArgumentException will be thrown when type is * null. *

    * @@ -182,7 +182,7 @@ public abstract class IQ extends Stanza implements IqView { // Add the query section if there is one. IQChildElementXmlStringBuilder iqChildElement = getIQChildElementBuilder( new IQChildElementXmlStringBuilder(getChildElementName(), getChildElementNamespace(), null, xml.getXmlEnvironment())); - // TOOD: Document the cases where iqChildElement is null but childElementName not. And if there are none, change + // TODO: Document the cases where iqChildElement is null but childElementName not. And if there are none, change // the logic. if (iqChildElement == null) { return; @@ -287,20 +287,6 @@ public abstract class IQ extends Stanza implements IqView { return ErrorIQ.createErrorResponse(request, error); } - /** - * Deprecated. - * - * @param request the request. - * @param error the error. - * @return an error IQ. - * @deprecated use {@link #createErrorResponse(IQ, StanzaError)} instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public static ErrorIQ createErrorResponse(final IQ request, final StanzaError.Builder error) { - return createErrorResponse(request, error.build()); - } - public static ErrorIQ createErrorResponse(final IQ request, final StanzaError.Condition condition) { return createErrorResponse(request, StanzaError.getBuilder(condition).build()); } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/Mechanisms.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/Mechanisms.java index a981589ee..2d36eb920 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/Mechanisms.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/Mechanisms.java @@ -1,6 +1,6 @@ /** * - * Copyright © 2014-2020 Florian Schmaus + * Copyright © 2014-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. @@ -16,9 +16,9 @@ */ package org.jivesoftware.smack.packet; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.LinkedList; import java.util.List; import javax.xml.namespace.QName; @@ -31,7 +31,7 @@ public class Mechanisms implements ExtensionElement { public static final String NAMESPACE = "urn:ietf:params:xml:ns:xmpp-sasl"; public static final QName QNAME = new QName(NAMESPACE, ELEMENT); - public final List mechanisms = new LinkedList(); + public final List mechanisms = new ArrayList(); public Mechanisms(String mechanism) { mechanisms.add(mechanism); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/Message.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/Message.java index d47cafda7..6063af13b 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/Message.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/Message.java @@ -17,7 +17,6 @@ package org.jivesoftware.smack.packet; -import java.util.List; import java.util.Locale; import javax.xml.namespace.QName; @@ -29,10 +28,6 @@ import org.jivesoftware.smack.util.Objects; import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.util.XmlStringBuilder; -import org.jxmpp.jid.Jid; -import org.jxmpp.jid.impl.JidCreate; -import org.jxmpp.stringprep.XmppStringprepException; - /** * Represents XMPP message packets. A message can be one of several types: * @@ -63,85 +58,7 @@ public final class Message extends MessageOrPresence public static final String ELEMENT = "message"; public static final String BODY = "body"; - private Type type; - - /** - * Creates a new, "normal" message. - * @deprecated use {@link StanzaBuilder}, preferable via {@link StanzaFactory}, instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public Message() { - } - - /** - * Creates a new "normal" message to the specified recipient. - * - * @param to the recipient of the message. - * @deprecated use {@link StanzaBuilder}, preferable via {@link StanzaFactory}, instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public Message(Jid to) { - setTo(to); - } - - /** - * Creates a new message of the specified type to a recipient. - * - * @param to the user to send the message to. - * @param type the message type. - * @deprecated use {@link StanzaBuilder}, preferable via {@link StanzaFactory}, instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public Message(Jid to, Type type) { - this(to); - setType(type); - } - - /** - * Creates a new message to the specified recipient and with the specified body. - * - * @param to the user to send the message to. - * @param body the body of the message. - * @deprecated use {@link StanzaBuilder}, preferable via {@link StanzaFactory}, instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public Message(Jid to, String body) { - this(to); - setBody(body); - } - - /** - * Creates a new message to the specified recipient and with the specified body. - * - * @param to the user to send the message to. - * @param body the body of the message. - * @throws XmppStringprepException if 'to' is not a valid XMPP address. - * @deprecated use {@link StanzaBuilder}, preferable via {@link StanzaFactory}, instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public Message(String to, String body) throws XmppStringprepException { - this(JidCreate.from(to), body); - } - - /** - * Creates a new message with the specified recipient and extension element. - * - * @param to TODO javadoc me please - * @param extensionElement TODO javadoc me please - * @since 4.2 - * @deprecated use {@link StanzaBuilder}, preferable via {@link StanzaFactory}, instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public Message(Jid to, ExtensionElement extensionElement) { - this(to); - addExtension(extensionElement); - } + private final Type type; Message(MessageBuilder messageBuilder) { super(messageBuilder); @@ -170,197 +87,6 @@ public final class Message extends MessageOrPresence return type; } - /** - * Sets the type of the message. - * - * @param type the type of the message. - * @deprecated use {@link StanzaBuilder} instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public void setType(Type type) { - this.type = type; - } - - /** - * Sets the subject of the message. The subject is a short description of - * message contents. - * - * @param subject the subject of the message. - * @deprecated use {@link StanzaBuilder} instead. - */ - @Deprecated - // TODO: Remove when stanza builder is ready. - public void setSubject(String subject) { - if (subject == null) { - removeSubject(""); // use empty string because #removeSubject(null) is ambiguous - return; - } - addSubject(null, subject); - } - - /** - * Adds a subject with a corresponding language. - * - * @param language the language of the subject being added. - * @param subject the subject being added to the message. - * @return the new {@link org.jivesoftware.smack.packet.Message.Subject} - * @throws NullPointerException if the subject is null, a null pointer exception is thrown - */ - @Deprecated - // TODO: Remove when stanza builder is ready. - public Subject addSubject(String language, String subject) { - language = Stanza.determineLanguage(this, language); - - List currentSubjects = getExtensions(Subject.class); - for (Subject currentSubject : currentSubjects) { - if (language.equals(currentSubject.getLanguage())) { - throw new IllegalArgumentException("Subject with the language " + language + " already exists"); - } - } - - Subject messageSubject = new Subject(language, subject); - addExtension(messageSubject); - return messageSubject; - } - - /** - * Removes the subject with the given language from the message. - * - * @param language the language of the subject which is to be removed - * @return true if a subject was removed and false if it was not. - */ - @Deprecated - // TODO: Remove when stanza builder is ready. - public boolean removeSubject(String language) { - language = Stanza.determineLanguage(this, language); - for (Subject subject : getExtensions(Subject.class)) { - if (language.equals(subject.language)) { - return removeSubject(subject); - } - } - return false; - } - - /** - * Removes the subject from the message and returns true if the subject was removed. - * - * @param subject the subject being removed from the message. - * @return true if the subject was successfully removed and false if it was not. - */ - @Deprecated - // TODO: Remove when stanza builder is ready. - public boolean removeSubject(Subject subject) { - return removeExtension(subject) != null; - } - - /** - * Sets the body of the message. - * - * @param body the body of the message. - * @see #setBody(String) - * @since 4.2 - * @deprecated use {@link StanzaBuilder} instead. - */ - @Deprecated - // TODO: Remove when stanza builder is ready. - public void setBody(CharSequence body) { - String bodyString; - if (body != null) { - bodyString = body.toString(); - } else { - bodyString = null; - } - setBody(bodyString); - } - - /** - * Sets the body of the message. The body is the main message contents. - * - * @param body the body of the message. - * @deprecated use {@link StanzaBuilder} instead. - */ - @Deprecated - // TODO: Remove when stanza builder is ready. - public void setBody(String body) { - if (body == null) { - removeBody(""); // use empty string because #removeBody(null) is ambiguous - return; - } - addBody(null, body); - } - - /** - * Adds a body with a corresponding language. - * - * @param language the language of the body being added. - * @param body the body being added to the message. - * @return the new {@link org.jivesoftware.smack.packet.Message.Body} - * @throws NullPointerException if the body is null, a null pointer exception is thrown - * @since 3.0.2 - * @deprecated use {@link StanzaBuilder} instead. - */ - @Deprecated - // TODO: Remove when stanza builder is ready. - public Body addBody(String language, String body) { - language = Stanza.determineLanguage(this, language); - - removeBody(language); - - Body messageBody = new Body(language, body); - addExtension(messageBody); - return messageBody; - } - - /** - * Removes the body with the given language from the message. - * - * @param language the language of the body which is to be removed - * @return true if a body was removed and false if it was not. - * @deprecated use {@link StanzaBuilder} instead. - */ - @Deprecated - // TODO: Remove when stanza builder is ready. - public boolean removeBody(String language) { - language = Stanza.determineLanguage(this, language); - for (Body body : getBodies()) { - String bodyLanguage = body.getLanguage(); - if (Objects.equals(bodyLanguage, language)) { - removeExtension(body); - return true; - } - } - return false; - } - - /** - * Removes the body from the message and returns true if the body was removed. - * - * @param body the body being removed from the message. - * @return true if the body was successfully removed and false if it was not. - * @since 3.0.2 - * @deprecated use {@link StanzaBuilder} instead. - */ - @Deprecated - // TODO: Remove when stanza builder is ready. - public boolean removeBody(Body body) { - XmlElement removedElement = removeExtension(body); - return removedElement != null; - } - - /** - * Sets the thread id of the message, which is a unique identifier for a sequence - * of "chat" messages. - * - * @param thread the thread id of the message. - * @deprecated use {@link StanzaBuilder} instead. - */ - @Deprecated - // TODO: Remove when stanza builder is ready. - public void setThread(String thread) { - addExtension(new Message.Thread(thread)); - } - @Override public String getElementName() { return ELEMENT; @@ -412,22 +138,6 @@ public final class Message extends MessageOrPresence return buf; } - /** - * Creates and returns a copy of this message stanza. - *

    - * This does not perform a deep clone, as extension elements are shared between the new and old - * instance. - *

    - * @return a clone of this message. - * @deprecated use {@link #asBuilder()} instead. - */ - // TODO: Remove in Smack 4.5. - @Deprecated - @Override - public Message clone() { - return new Message(this); - } - /** * Represents a message subject, its language and the content of the subject. */ diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/MessageOrPresence.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/MessageOrPresence.java index f48b794a0..c3aa3d180 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/MessageOrPresence.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/MessageOrPresence.java @@ -20,11 +20,6 @@ import org.jivesoftware.smack.XMPPConnection; public abstract class MessageOrPresence> extends Stanza { - @Deprecated - // TODO: Remove in Smack 4.5. - protected MessageOrPresence() { - } - protected MessageOrPresence(StanzaBuilder stanzaBuilder) { super(stanzaBuilder); } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/Nonza.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/Nonza.java index a86a4e1db..ece665543 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/Nonza.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/Nonza.java @@ -18,7 +18,7 @@ package org.jivesoftware.smack.packet; /** - * A Nonza, i.e everything that is not a stanza as defined + * A Nonza, i.e. everything that is not a stanza as defined * RFC 6120 8. Stanzas are {@link Message}, {@link Presence} and {@link IQ}. * Everything else should sublcass this class instead of {@link Stanza}. *

    diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/Presence.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/Presence.java index 3490e1a23..caa3942dc 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/Presence.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/Presence.java @@ -1,6 +1,6 @@ /** * - * Copyright 2003-2007 Jive Software, 2020-2021 Florian Schmaus. + * Copyright 2003-2007 Jive Software, 2020-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. @@ -21,12 +21,9 @@ import java.util.List; import java.util.Locale; import org.jivesoftware.smack.XMPPConnection; -import org.jivesoftware.smack.util.Objects; import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.util.XmlStringBuilder; -import org.jxmpp.jid.Jid; - /** * Represents XMPP presence stanzas. Every presence stanza has a type, which is one of * the following values: @@ -78,55 +75,6 @@ public final class Presence extends MessageOrPresence private Mode mode = null; - /** - * Creates a new presence update. Status, priority, and mode are left un-set. - * - * @param type the type. - * @deprecated use {@link PresenceBuilder} or {@link org.jivesoftware.smack.XMPPConnection#getStanzaFactory} instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public Presence(Type type) { - // Ensure that the stanza ID is set by calling super(). - super(); - setType(type); - } - - /** - * Creates a new presence with the given type and using the given XMPP address as recipient. - * - * @param to the recipient. - * @param type the type. - * @since 4.2 - * @deprecated use {@link PresenceBuilder} or {@link org.jivesoftware.smack.XMPPConnection#getStanzaFactory} instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public Presence(Jid to, Type type) { - this(type); - setTo(to); - } - - /** - * Creates a new presence update with a specified status, priority, and mode. - * - * @param type the type. - * @param status a text message describing the presence update. - * @param priority the priority of this presence update. - * @param mode the mode type for this presence update. - * @deprecated use {@link PresenceBuilder} or {@link org.jivesoftware.smack.XMPPConnection#getStanzaFactory} instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public Presence(Type type, String status, int priority, Mode mode) { - // Ensure that the stanza ID is set by calling super(). - super(); - setType(type); - setStatus(status); - setPriority(priority); - setMode(mode); - } - Presence(PresenceBuilder presenceBuilder) { super(presenceBuilder); type = presenceBuilder.type; @@ -186,36 +134,11 @@ public final class Presence extends MessageOrPresence return type; } - /** - * Sets the type of the presence packet. - * - * @param type the type of the presence packet. - * @deprecated use {@link PresenceBuilder} or {@link org.jivesoftware.smack.XMPPConnection#getStanzaFactory} instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public void setType(Type type) { - this.type = Objects.requireNonNull(type, "Type cannot be null"); - } - @Override public String getStatus() { return status; } - /** - * Sets the status message of the presence update. The status is free-form text - * describing a user's presence (i.e., "gone to lunch"). - * - * @param status the status message. - * @deprecated use {@link PresenceBuilder} or {@link org.jivesoftware.smack.XMPPConnection#getStanzaFactory} instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public void setStatus(String status) { - this.status = status; - } - @Override public int getPriority() { return getPriorityByte(); @@ -233,20 +156,11 @@ public final class Presence extends MessageOrPresence * Sets the priority of the presence. The valid range is -128 through 127. * * @param priority the priority of the presence. - * @throws IllegalArgumentException if the priority is outside the valid range. * @see RFC 6121 § 4.7.2.3. Priority Element * @deprecated use {@link PresenceBuilder} or {@link org.jivesoftware.smack.XMPPConnection#getStanzaFactory} instead. */ @Deprecated - // TODO: Remove in Smack 4.5. - public void setPriority(int priority) { - if (priority < -128 || priority > 127) { - throw new IllegalArgumentException("Priority value " + priority + - " is not valid. Valid range is -128 through 127."); - } - setPriority((byte) priority); - } - + // TODO: Remove in Smack 4.6. public void setPriority(byte priority) { this.priority = priority; } @@ -259,19 +173,6 @@ public final class Presence extends MessageOrPresence return mode; } - /** - * Sets the mode of the presence update. A null presence mode value is interpreted - * to be the same thing as {@link Presence.Mode#available}. - * - * @param mode the mode. - * @deprecated use {@link PresenceBuilder} or {@link org.jivesoftware.smack.XMPPConnection#getStanzaFactory} instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public void setMode(Mode mode) { - this.mode = mode; - } - @Override public String getElementName() { return ELEMENT; @@ -346,37 +247,6 @@ public final class Presence extends MessageOrPresence return buf; } - /** - * Creates and returns a copy of this presence stanza. - *

    - * This does not perform a deep clone, as extension elements are shared between the new and old - * instance. - *

    - * @return a clone of this presence. - * @deprecated use {@link #asBuilder()} instead. - */ - // TODO: Remove in Smack 4.5. - @Deprecated - @Override - public Presence clone() { - return new Presence(this); - } - - /** - * Clone this presence and set a newly generated stanza ID as the clone's ID. - * - * @return a "clone" of this presence with a different stanza ID. - * @since 4.1.2 - * @deprecated use {@link #asBuilder(XMPPConnection)} or {@link #asBuilder(String)}instead. - */ - // TODO: Remove in Smack 4.5. - @Deprecated - public Presence cloneWithNewId() { - Presence clone = clone(); - clone.setNewStanzaId(); - return clone; - } - /** * An enum to represent the presence type. Note that presence type is often confused * with presence mode. Generally, if a user is signed in to a server, they have a presence diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/Session.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/Session.java index 48e0330c6..7e6331bb8 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/Session.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/Session.java @@ -39,6 +39,7 @@ public class Session extends SimpleIQ { public static final String ELEMENT = "session"; public static final String NAMESPACE = "urn:ietf:params:xml:ns:xmpp-session"; + @SuppressWarnings("this-escape") public Session() { super(ELEMENT, NAMESPACE); setType(IQ.Type.set); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/StandardExtensionElement.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/StandardExtensionElement.java index 8ae1f5a7a..9f1959460 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/StandardExtensionElement.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/StandardExtensionElement.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2021 Florian Schmaus. + * Copyright 2015-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. @@ -53,9 +53,7 @@ public final class StandardExtensionElement implements XmlElement { /** * Constructs a new extension element with the given name and namespace and nothing else. - *

    * This is meant to construct extension elements used as simple flags in Stanzas. - *

    * * @param name the name of the extension element. * @param namespace the namespace of the extension element. diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/Stanza.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/Stanza.java index 7123e82f8..88c30bbfa 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/Stanza.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/Stanza.java @@ -98,7 +98,7 @@ public abstract class Stanza implements StanzaView, TopLevelStreamElement { protected Stanza(StanzaBuilder stanzaBuilder) { if (stanzaBuilder.stanzaIdSource != null) { id = stanzaBuilder.stanzaIdSource.getNewStanzaId(); - // Note that some stanza ID sources, e.g. StanzaBuilder.PresenceBuilder.EMPTY return null here. Hence we + // Note that some stanza ID sources, e.g. StanzaBuilder.PresenceBuilder.EMPTY return null here. Hence, we // only check that the returned string is not empty. assert StringUtils.isNullOrNotEmpty(id); usedStanzaIdSource = stanzaBuilder.stanzaIdSource; @@ -159,22 +159,6 @@ public abstract class Stanza implements StanzaView, TopLevelStreamElement { return id != null; } - /** - * Set the stanza id if none is set. - * - * @return the stanza id. - * @since 4.2 - * @deprecated use {@link StanzaBuilder} instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public String setStanzaId() { - if (!hasStanzaIdSet()) { - setNewStanzaId(); - } - return getStanzaId(); - } - /** * Throws an {@link IllegalArgumentException} if this stanza has no stanza ID set. * @@ -219,7 +203,7 @@ public abstract class Stanza implements StanzaView, TopLevelStreamElement { * @param to who the packet is being sent to. */ // TODO: Mark this as deprecated once StanzaBuilder is ready and all call sites are gone. - public void setTo(Jid to) { + public final void setTo(Jid to) { this.to = to; } @@ -255,34 +239,11 @@ public abstract class Stanza implements StanzaView, TopLevelStreamElement { error = stanzaError; } - /** - * Deprecated. - * @param stanzaError the stanza error. - * @deprecated use {@link StanzaBuilder} instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public void setError(StanzaError.Builder stanzaError) { - setError(stanzaError.build()); - } - @Override public final String getLanguage() { return language; } - /** - * Sets the xml:lang of this Stanza. - * - * @param language the xml:lang of this Stanza. - * @deprecated use {@link StanzaBuilder#setLanguage(String)} instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public void setLanguage(String language) { - this.language = language; - } - @Override public final List getExtensions() { synchronized (extensionElements) { @@ -374,22 +335,6 @@ public abstract class Stanza implements StanzaView, TopLevelStreamElement { return packetExtension; } - /** - * This method is deprecated. Use preferably {@link #getExtension(Class)} or {@link #getExtensionElement(String, String)}. - * - * @param the type to cast to. - * @param elementName the XML element name of the extension. (May be null) - * @param namespace the XML element namespace of the extension. - * @return the extension, or null if it doesn't exist. - * @deprecated use {@link #getExtension(Class)} or {@link #getExtensionElement(String, String)} instead. - */ - // TODO: Remove in Smack 4.5. - @SuppressWarnings("unchecked") - @Deprecated - public final E getExtension(String elementName, String namespace) { - return (E) getExtensionElement(elementName, namespace); - } - @Override public final XmlElement getExtension(QName qname) { synchronized (extensionElements) { @@ -501,27 +446,6 @@ public abstract class Stanza implements StanzaView, TopLevelStreamElement { } } - /** - * Removes a stanza extension from the packet. - * - * @param extension the stanza extension to remove. - * @return the removed stanza extension or null. - * @deprecated use {@link StanzaBuilder} instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public final XmlElement removeExtension(XmlElement extension) { - QName key = extension.getQName(); - synchronized (extensionElements) { - List list = extensionElements.getAll(key); - boolean removed = list.remove(extension); - if (removed) { - return extension; - } - } - return null; - } - /** * Returns a short String describing the Stanza. This method is suited for log purposes. */ diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/StanzaBuilder.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/StanzaBuilder.java index ec8bdac3c..2cc2a20ed 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/StanzaBuilder.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/StanzaBuilder.java @@ -87,9 +87,9 @@ public abstract class StanzaBuilder> implements Stanz } /** - * Set the recipent address of the stanza. + * Set the recipient address of the stanza. * - * @param to whoe the stanza is being sent to. + * @param to whom the stanza is being sent. * @return a reference to this builder. * @throws XmppStringprepException if the provided character sequence is not a valid XMPP address. * @see #to(Jid) diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/StanzaView.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/StanzaView.java index 85c4af1a9..e435d7159 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/StanzaView.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/StanzaView.java @@ -1,6 +1,6 @@ /** * - * Copyright 2019-2021 Florian Schmaus + * Copyright 2019-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. @@ -36,7 +36,7 @@ public interface StanzaView extends XmlLangElement { /** * Returns who the stanza is being sent "to", or null if * the value is not set. The XMPP protocol often makes the "to" - * attribute optional, so it does not always need to be set.

    + * attribute optional, so it does not always need to be set. * * @return who the stanza is being sent to, or null if the * value has not been set. @@ -46,7 +46,7 @@ public interface StanzaView extends XmlLangElement { /** * Returns who the stanza is being sent "from" or null if * the value is not set. The XMPP protocol often makes the "from" - * attribute optional, so it does not always need to be set.

    + * attribute optional, so it does not always need to be set. * * @return who the stanza is being sent from, or null if the * value has not been set. diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/StreamError.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/StreamError.java index f034be9d3..a58599de8 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/StreamError.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/StreamError.java @@ -58,9 +58,9 @@ import org.jivesoftware.smack.util.XmlStringBuilder; * stream has been authenticated * policy-violation the entity has violated some local service * policy. - * remote-connection-failed Rthe server is unable to properly connect + * remote-connection-failed the server is unable to properly connect * to a remote entity. - * resource-constraint Rthe server lacks the system resources necessary + * resource-constraint the server lacks the system resources necessary * to service the stream. * restricted-xml the entity has attempted to send restricted XML * features. diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/UnparsedIQ.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/UnparsedIQ.java index e363d2e30..8cd2146f8 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/UnparsedIQ.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/UnparsedIQ.java @@ -16,6 +16,8 @@ */ package org.jivesoftware.smack.packet; +import org.jivesoftware.smack.util.StringUtils; + /** * An IQ stanzas that could not be parsed because no provider was found. */ @@ -34,7 +36,12 @@ public class UnparsedIQ extends IQ { @Override protected IQChildElementXmlStringBuilder getIQChildElementBuilder(IQChildElementXmlStringBuilder xml) { - xml.escape(content); + if (StringUtils.isEmpty(content)) { + xml.setEmptyElement(); + } else { + xml.rightAngleBracket(); + xml.escape(content); + } return xml; } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/provider/ProviderFileLoader.java b/smack-core/src/main/java/org/jivesoftware/smack/provider/ProviderFileLoader.java index 11da95318..2781fe0d9 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/provider/ProviderFileLoader.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/provider/ProviderFileLoader.java @@ -17,9 +17,9 @@ package org.jivesoftware.smack.provider; import java.io.InputStream; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.LinkedList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; @@ -39,11 +39,11 @@ import org.jivesoftware.smack.xml.XmlPullParser; public class ProviderFileLoader implements ProviderLoader { private static final Logger LOGGER = Logger.getLogger(ProviderFileLoader.class.getName()); - private final Collection iqProviders = new LinkedList(); - private final Collection extProviders = new LinkedList(); - private final Collection sfProviders = new LinkedList(); + private final Collection iqProviders = new ArrayList(); + private final Collection extProviders = new ArrayList(); + private final Collection sfProviders = new ArrayList(); - private List exceptions = new LinkedList(); + private List exceptions = new ArrayList(); public ProviderFileLoader(InputStream providerStream) { this(providerStream, ProviderFileLoader.class.getClassLoader()); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/provider/ProviderManager.java b/smack-core/src/main/java/org/jivesoftware/smack/provider/ProviderManager.java index 0943f31d6..b2f08cc3a 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/provider/ProviderManager.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/provider/ProviderManager.java @@ -97,6 +97,7 @@ import org.jivesoftware.smack.util.XmppElementUtil; * </extensionProvider> * </smackProviders> * + *

    * If multiple provider entries attempt to register to handle the same element name and namespace, * the first entry loaded from the classpath will take precedence. Whenever a stanza extension * is found in a packet, parsing will be passed to the correct provider. Each provider @@ -106,7 +107,8 @@ import org.jivesoftware.smack.util.XmppElementUtil; * set the properties of th class using the values in the stanza extension sub-element. When an * extension provider is not registered for an element name and namespace combination, Smack will * store all top-level elements of the sub-packet in DefaultPacketExtension object and then - * attach it to the packet.

    + * attach it to the packet. + *

    * * @author Matt Tucker */ diff --git a/smack-core/src/main/java/org/jivesoftware/smack/provider/package-info.java b/smack-core/src/main/java/org/jivesoftware/smack/provider/package-info.java index 5e40c5639..21fafdfcb 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/provider/package-info.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/provider/package-info.java @@ -16,7 +16,7 @@ */ /** - * The Smack provider architecture is a system for plugging in custom XML parsing of staza extensions + * The Smack provider architecture is a system for plugging in custom XML parsing of stanza extensions * ({@link org.jivesoftware.smack.packet.ExtensionElement}, {@link org.jivesoftware.smack.packet.IQ} stanzas and * {@link org.jivesoftware.smack.packet.Nonza}. Hence, there are the the following providers: *
      diff --git a/smack-core/src/main/java/org/jivesoftware/smack/proxy/HTTPProxySocketConnection.java b/smack-core/src/main/java/org/jivesoftware/smack/proxy/HTTPProxySocketConnection.java index 549406423..bdf0cf390 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/proxy/HTTPProxySocketConnection.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/proxy/HTTPProxySocketConnection.java @@ -23,6 +23,7 @@ import java.io.StringReader; import java.net.HttpURLConnection; import java.net.InetSocketAddress; import java.net.Socket; +import java.nio.charset.StandardCharsets; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -58,7 +59,7 @@ class HTTPProxySocketConnection implements ProxySocketConnection { proxyLine = "\r\nProxy-Authorization: Basic " + Base64.encode(username + ":" + password); } socket.getOutputStream().write((hostport + " HTTP/1.1\r\nHost: " - + host + ":" + port + proxyLine + "\r\n\r\n").getBytes("UTF-8")); + + host + ":" + port + proxyLine + "\r\n\r\n").getBytes(StandardCharsets.UTF_8)); InputStream in = socket.getInputStream(); StringBuilder got = new StringBuilder(100); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/proxy/ProxyInfo.java b/smack-core/src/main/java/org/jivesoftware/smack/proxy/ProxyInfo.java index 402a106a4..90433c5c8 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/proxy/ProxyInfo.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/proxy/ProxyInfo.java @@ -41,6 +41,7 @@ public class ProxyInfo { private ProxyType proxyType; private final ProxySocketConnection proxySocketConnection; + @SuppressWarnings("this-escape") public ProxyInfo(ProxyType pType, String pHost, int pPort, String pUser, String pPass) { this.proxyType = pType; diff --git a/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLMechanism.java b/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLMechanism.java index f1c00ef19..24330a6ac 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLMechanism.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLMechanism.java @@ -358,7 +358,7 @@ public abstract class SASLMechanism implements Comparable { * SASLprep the given String. The resulting String is in UTF-8. * * @param string the String to sasl prep. - * @return the given String SASL preped + * @return the given String SASL prepped * @see RFC 4013 - SASLprep: Stringprep Profile for User Names and Passwords */ protected static String saslPrep(String string) { diff --git a/smack-core/src/main/java/org/jivesoftware/smack/sasl/core/ScramMechanism.java b/smack-core/src/main/java/org/jivesoftware/smack/sasl/core/ScramMechanism.java index 8f4133c81..6dc482ce3 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/sasl/core/ScramMechanism.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/sasl/core/ScramMechanism.java @@ -271,6 +271,7 @@ public abstract class ScramMechanism extends SASLMechanism { return null; } + @SuppressWarnings("MixedMutabilityReturnType") private static Map parseAttributes(String string) throws SmackSaslException { if (string.length() == 0) { return Collections.emptyMap(); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/CollectionUtil.java b/smack-core/src/main/java/org/jivesoftware/smack/util/CollectionUtil.java index 408e00bfd..c3566c6c0 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/CollectionUtil.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/CollectionUtil.java @@ -55,6 +55,7 @@ public class CollectionUtil { boolean test(T t); } + @SuppressWarnings("NonApiType") public static ArrayList newListWith(Collection collection) { if (collection == null) { return null; 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/MAC.java b/smack-core/src/main/java/org/jivesoftware/smack/util/MAC.java index 4d78d24f8..df177204e 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/MAC.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/MAC.java @@ -33,7 +33,7 @@ public class MAC { HMAC_SHA1 = Mac.getInstance(HMACSHA1); } catch (NoSuchAlgorithmException e) { - // Smack wont be able to function normally if this exception is thrown, wrap it into + // Smack won't be able to function normally if this exception is thrown, wrap it into // an ISE and make the user aware of the problem. throw new IllegalStateException(e); } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/MD5.java b/smack-core/src/main/java/org/jivesoftware/smack/util/MD5.java index 26c0547fd..d81c4de5a 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/MD5.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/MD5.java @@ -31,7 +31,7 @@ public class MD5 { MD5_DIGEST = MessageDigest.getInstance(StringUtils.MD5); } catch (NoSuchAlgorithmException e) { - // Smack wont be able to function normally if this exception is thrown, wrap it into + // Smack won't be able to function normally if this exception is thrown, wrap it into // an ISE and make the user aware of the problem. throw new IllegalStateException(e); } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/MultiMap.java b/smack-core/src/main/java/org/jivesoftware/smack/util/MultiMap.java index 852e01bf7..7e9889e57 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/MultiMap.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/MultiMap.java @@ -1,6 +1,6 @@ /** * - * Copyright © 2015-2021 Florian Schmaus + * Copyright © 2015-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. @@ -184,13 +184,14 @@ public class MultiMap { } /** - * Remove the given number of values for a given key. May return less values then requested. + * Remove the given number of values for a given key. May return less values than requested. * * @param key the key to remove from. * @param num the number of values to remove. * @return a list of the removed values. * @since 4.4.0 */ + @SuppressWarnings("MixedMutabilityReturnType") public List remove(K key, int num) { List values = map.get(key); if (values == null) { diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/NumberUtil.java b/smack-core/src/main/java/org/jivesoftware/smack/util/NumberUtil.java index 98635309d..836796248 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/NumberUtil.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/NumberUtil.java @@ -1,6 +1,6 @@ /** * - * Copyright © 2015-2020 Florian Schmaus + * Copyright © 2015-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. @@ -18,18 +18,6 @@ package org.jivesoftware.smack.util; public class NumberUtil { - /** - * Checks if the given long is within the range of an unsigned 32-bit integer, the XML type "xs:unsignedInt". - * - * @param value TODO javadoc me please - * @deprecated use {@link #requireUInt32(long)} instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public static void checkIfInUInt32Range(long value) { - requireUInt32(value); - } - /** * Checks if the given long is within the range of an unsigned 32-bit integer, the XML type "xs:unsignedInt". * diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/PacketParserUtils.java b/smack-core/src/main/java/org/jivesoftware/smack/util/PacketParserUtils.java index 042402299..69f0aaca2 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/PacketParserUtils.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/PacketParserUtils.java @@ -1,6 +1,6 @@ /** * - * Copyright 2003-2007 Jive Software, 2019-2023 Florian Schmaus. + * Copyright 2003-2007 Jive Software, 2019-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. @@ -25,7 +25,6 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.logging.Level; @@ -87,7 +86,7 @@ public class PacketParserUtils { return parser; } - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) public static S parseStanza(String stanza) throws XmlPullParserException, SmackParsingException, IOException { return (S) parseStanza(getParserFor(stanza), XmlEnvironment.EMPTY); } @@ -230,7 +229,7 @@ public class PacketParserUtils { // Assume this is the end tag of the start tag at the // beginning of this method. Typical examples where this // happens are body elements containing the empty string, - // ie. , which appears to be valid XMPP, or a + // i.e. , which appears to be valid XMPP, or a // least it's not explicitly forbidden by RFC 6121 5.2.3 return ""; } else { @@ -644,7 +643,7 @@ public class PacketParserUtils { assert parser.getEventType() == XmlPullParser.Event.START_ELEMENT; String name; final int initialDepth = parser.getDepth(); - List methods = new LinkedList<>(); + List methods = new ArrayList<>(); outerloop: while (true) { XmlPullParser.Event eventType = parser.next(); switch (eventType) { @@ -850,7 +849,7 @@ public class PacketParserUtils { throws XmlPullParserException, IOException { ParserUtils.assertAtStartTag(parser); assert parser.getNamespace().equals(StartTls.NAMESPACE); - int initalDepth = parser.getDepth(); + int initialDepth = parser.getDepth(); boolean required = false; outerloop: while (true) { XmlPullParser.Event event = parser.next(); @@ -864,7 +863,7 @@ public class PacketParserUtils { } break; case END_ELEMENT: - if (parser.getDepth() == initalDepth) { + if (parser.getDepth() == initialDepth) { break outerloop; } break; diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/PacketUtil.java b/smack-core/src/main/java/org/jivesoftware/smack/util/PacketUtil.java index a68e91dd0..40da6f4d6 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/PacketUtil.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/PacketUtil.java @@ -1,6 +1,6 @@ /** * - * Copyright © 2014-2021 Florian Schmaus + * Copyright © 2014-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. @@ -32,7 +32,7 @@ public class PacketUtil { * * @return the extension element */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) public static PE extensionElementFrom(Collection collection, String element, String namespace) { for (XmlElement packetExtension : collection) { diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/Pair.java b/smack-core/src/main/java/org/jivesoftware/smack/util/Pair.java index 08bf41234..13e65e881 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/Pair.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/Pair.java @@ -1,6 +1,6 @@ /** * - * Copyright 2020 Florian Schmaus. + * Copyright 2020-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. @@ -26,11 +26,12 @@ public final class Pair { this.second = second; } - public static Pair create(F first, S second) { + public static Pair create(F first, S second) { return new Pair<>(first, second); } - public static Pair createAndInitHashCode(F first, S second) { + @SuppressWarnings("ReturnValueIgnored") + public static Pair createAndInitHashCode(F first, S second) { Pair pair = new Pair<>(first, second); pair.hashCode(); return pair; diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/ParserUtils.java b/smack-core/src/main/java/org/jivesoftware/smack/util/ParserUtils.java index 64c18a7fb..7c58f8a12 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/ParserUtils.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/ParserUtils.java @@ -1,6 +1,6 @@ /** * - * Copyright © 2014-2023 Florian Schmaus + * Copyright © 2014-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. @@ -23,8 +23,6 @@ import java.text.ParseException; import java.util.Date; import java.util.Locale; -import javax.xml.namespace.QName; - import org.jivesoftware.smack.datatypes.UInt16; import org.jivesoftware.smack.datatypes.UInt32; import org.jivesoftware.smack.packet.XmlEnvironment; @@ -146,7 +144,7 @@ public class ParserUtils { } /** - * Prase a string to a boolean value as per "xs:boolean". Valid input strings are "true", "1" for true, and "false", "0" for false. + * Phrase a string to a boolean value as per "xs:boolean". Valid input strings are "true", "1" for true, and "false", "0" for false. * * @param booleanString the input string. * @return the boolean representation of the input string @@ -367,19 +365,6 @@ public class ParserUtils { return parser.getAttributeValue("http://www.w3.org/XML/1998/namespace", "lang"); } - /** - * Get the QName of the current element. - * - * @param parser the parser. - * @return the Qname. - * @deprecated use {@link XmlPullParser#getQName()} instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5 - public static QName getQName(XmlPullParser parser) { - return parser.getQName(); - } - public static InternetAddress getInternetAddressIngoringZoneIdAttribute(XmlPullParser parser, String attribute) { String inetAddressString = parser.getAttributeValue(attribute); if (inetAddressString == null) { diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/SHA1.java b/smack-core/src/main/java/org/jivesoftware/smack/util/SHA1.java index 89144530b..3c7d53ac4 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/SHA1.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/SHA1.java @@ -31,7 +31,7 @@ public class SHA1 { SHA1_DIGEST = MessageDigest.getInstance(StringUtils.SHA1); } catch (NoSuchAlgorithmException e) { - // Smack wont be able to function normally if this exception is thrown, wrap it into + // Smack won't be able to function normally if this exception is thrown, wrap it into // an ISE and make the user aware of the problem. throw new IllegalStateException(e); } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/SecurityUtil.java b/smack-core/src/main/java/org/jivesoftware/smack/util/SecurityUtil.java index 3521279df..c796ab23e 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/SecurityUtil.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/SecurityUtil.java @@ -1,6 +1,6 @@ /** * - * Copyright 2019 Florian Schmaus. + * Copyright 2019-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. @@ -26,6 +26,7 @@ public class SecurityUtil { private static final LruCache, Void> INSERTED_PROVIDERS_CACHE = new LruCache<>(8); + @SuppressWarnings("LockOnNonEnclosingClassLiteral") public static void ensureProviderAtFirstPosition(Class providerClass) { if (INSERTED_PROVIDERS_CACHE.containsKey(providerClass)) { return; @@ -41,7 +42,7 @@ public class SecurityUtil { String providerName = provider.getName(); - int installedPosition ; + int installedPosition; synchronized (Security.class) { Security.removeProvider(providerName); installedPosition = Security.insertProviderAt(provider, 1); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/StringUtils.java b/smack-core/src/main/java/org/jivesoftware/smack/util/StringUtils.java index 5ebe4c116..83c9f40bb 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/StringUtils.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/StringUtils.java @@ -1,6 +1,6 @@ /** * - * Copyright 2003-2007 Jive Software, 2016-2021 Florian Schmaus. + * Copyright 2003-2007 Jive Software, 2016-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. @@ -36,24 +36,6 @@ public class StringUtils { public static final String MD5 = "MD5"; public static final String SHA1 = "SHA-1"; - /** - * Deprecated, do not use. - * - * @deprecated use StandardCharsets.UTF_8 instead. - */ - // TODO: Remove in Smack 4.5. - @Deprecated - public static final String UTF8 = "UTF-8"; - - /** - * Deprecated, do not use. - * - * @deprecated use StandardCharsets.US_ASCII instead. - */ - // TODO: Remove in Smack 4.5. - @Deprecated - public static final String USASCII = "US-ASCII"; - public static final String QUOTE_ENCODE = """; public static final String APOS_ENCODE = "'"; public static final String AMP_ENCODE = "&"; @@ -343,11 +325,14 @@ public class StringUtils { try { randomString(charBuffer, random, alphabet, numRandomChars); } catch (IOException e) { - // This should never happen if we calcuate the buffer size correctly. + // This should never happen if we calculate the buffer size correctly. throw new AssertionError(e); } - return charBuffer.flip().toString(); + // Workaround for Android API not matching Java >=9 API. + // See https://issuetracker.google.com/issues/369219141 + ((java.nio.Buffer) charBuffer).flip(); + return charBuffer.toString(); } private static void randomString(Appendable appendable, Random random, char[] alphabet, int numRandomChars) @@ -479,7 +464,7 @@ public class StringUtils { appendTo(collection, ", ", sb); } - public static void appendTo(Collection collection, StringBuilder sb, + public static void appendTo(Collection collection, StringBuilder sb, Consumer appendFunction) { appendTo(collection, ", ", sb, appendFunction); } @@ -488,7 +473,7 @@ public class StringUtils { appendTo(collection, delimiter, sb, o -> sb.append(o)); } - public static void appendTo(Collection collection, String delimiter, StringBuilder sb, + public static void appendTo(Collection collection, String delimiter, StringBuilder sb, Consumer appendFunction) { for (Iterator it = collection.iterator(); it.hasNext();) { O cs = it.next(); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/TLSUtils.java b/smack-core/src/main/java/org/jivesoftware/smack/util/TLSUtils.java index c1af8ba61..47469f721 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/TLSUtils.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/TLSUtils.java @@ -1,6 +1,6 @@ /** * - * Copyright 2014-2020 Florian Schmaus + * Copyright 2014-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. @@ -68,52 +68,6 @@ public class TLSUtils { return builder; } - /** - * Enable only TLS. Connections created with the given ConnectionConfiguration will only support TLS. - *

      - * According to the Encrypted - * XMPP Manifesto, TLSv1.2 shall be deployed, providing fallback support for SSLv3 and - * TLSv1.1. This method goes one step beyond and upgrades the handshake to use TLSv1 or better. - * This method requires the underlying OS to support all of TLSv1.2 , 1.1 and 1.0. - *

      - * - * @param builder the configuration builder to apply this setting to - * @param Type of the ConnectionConfiguration builder. - * - * @return the given builder - * @deprecated use {@link #setEnabledTlsProtocolsToRecommended(org.jivesoftware.smack.ConnectionConfiguration.Builder)} instead. - */ - // TODO: Remove in Smack 4.5. - @Deprecated - public static > B setTLSOnly(B builder) { - builder.setEnabledSSLProtocols(new String[] { PROTO_TLSV1_2, PROTO_TLSV1_1, PROTO_TLSV1 }); - return builder; - } - - /** - * Enable only TLS and SSLv3. Connections created with the given ConnectionConfiguration will - * only support TLS and SSLv3. - *

      - * According to the Encrypted - * XMPP Manifesto, TLSv1.2 shall be deployed, providing fallback support for SSLv3 and - * TLSv1.1. - *

      - * - * @param builder the configuration builder to apply this setting to - * @param Type of the ConnectionConfiguration builder. - * - * @return the given builder - * @deprecated use {@link #setEnabledTlsProtocolsToRecommended(org.jivesoftware.smack.ConnectionConfiguration.Builder)} instead. - */ - // TODO: Remove in Smack 4.5. - @Deprecated - public static > B setSSLv3AndTLSOnly(B builder) { - builder.setEnabledSSLProtocols(new String[] { PROTO_TLSV1_2, PROTO_TLSV1_1, PROTO_TLSV1, PROTO_SSL3 }); - return builder; - } - /** * Accept all TLS certificates. *

      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..5c327157b 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-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. @@ -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 { @@ -46,6 +47,7 @@ public class XmlStringBuilder implements Appendable, CharSequence, Element { this(pe, null); } + @SuppressWarnings("this-escape") public XmlStringBuilder(NamedElement e) { this(); halfOpenElement(e.getElementName()); @@ -55,6 +57,7 @@ public class XmlStringBuilder implements Appendable, CharSequence, Element { this(element.getElementName(), element.getNamespace(), element.getLanguage(), enclosingXmlEnvironment); } + @SuppressWarnings("this-escape") public XmlStringBuilder(String elementName, String xmlNs, String xmlLang, XmlEnvironment enclosingXmlEnvironment) { sb = new LazyStringBuilder(); halfOpenElement(elementName); @@ -137,20 +140,6 @@ public class XmlStringBuilder implements Appendable, CharSequence, Element { return this; } - /** - * Deprecated. - * - * @param element deprecated. - * @return deprecated. - * @deprecated use {@link #append(Element)} instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public XmlStringBuilder element(Element element) { - assert element != null; - return append(element.toXML()); - } - public XmlStringBuilder optElement(String name, String content) { if (content != null) { element(name, content); @@ -311,6 +300,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 +594,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-core/src/main/java/org/jivesoftware/smack/util/XmppElementUtil.java b/smack-core/src/main/java/org/jivesoftware/smack/util/XmppElementUtil.java index 5639a7e5f..7ffe2f2eb 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/XmppElementUtil.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/XmppElementUtil.java @@ -71,6 +71,7 @@ public class XmppElementUtil { return qname; } + @SuppressWarnings("MixedMutabilityReturnType") public static List getElementsFrom( MultiMap elementMap, Class extensionElementClass) { QName qname = XmppElementUtil.getQNameFor(extensionElementClass); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/dns/package-info.java b/smack-core/src/main/java/org/jivesoftware/smack/util/dns/package-info.java index f06cdab84..1fb7e9b3b 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/dns/package-info.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/dns/package-info.java @@ -61,8 +61,8 @@ *

      *

      Best Practices

      *

      - * We recommend that applications using Smack's DNSSEC API do not ask the user if DNSSEC is avaialble. Instead they - * should check for DNSSEC suport on every connection attempt. Once DNSSEC support has been discovered, the application + * We recommend that applications using Smack's DNSSEC API do not ask the user if DNSSEC is available. Instead they + * should check for DNSSEC support on every connection attempt. Once DNSSEC support has been discovered, the application * should use the `needsDnssec` mode for all future connection attempts. The same scheme can be applied when using DANE. * This approach is similar to the scheme established by to HTTP Strict * Transport Security" (HSTS, RFC 6797. diff --git a/smack-core/src/test/java/org/jivesoftware/smack/packet/IqTest.java b/smack-core/src/test/java/org/jivesoftware/smack/packet/IqTest.java index b8f192bd7..2101daca4 100644 --- a/smack-core/src/test/java/org/jivesoftware/smack/packet/IqTest.java +++ b/smack-core/src/test/java/org/jivesoftware/smack/packet/IqTest.java @@ -1,6 +1,6 @@ /** * - * Copyright © 2023 Florian Schmaus + * Copyright © 2023-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. @@ -18,7 +18,13 @@ package org.jivesoftware.smack.packet; import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; +import org.jivesoftware.smack.test.util.SmackTestUtil; +import org.jivesoftware.smack.util.PacketParserUtils; +import org.jivesoftware.smack.xml.XmlPullParser; + import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; public class IqTest { @@ -35,4 +41,36 @@ public class IqTest { assertXmlSimilar(expected, errorIq.toXML()); } + @ParameterizedTest + @EnumSource(SmackTestUtil.XmlPullParserKind.class) + public void testIqWithXmlns(SmackTestUtil.XmlPullParserKind parserKind) throws Exception { + final String iqXml = "" + + "" + + "foo@tigase.mydomain.org/myresource" + + "" + + ""; + final String xml = + "" + + iqXml + + ""; + + XmlPullParser parser = SmackTestUtil.getParserFor(xml, "iq", parserKind); + IQ iq = PacketParserUtils.parseIQ(parser); + assertXmlSimilar(iqXml, iq.toXML()); + } + + @ParameterizedTest + @EnumSource(SmackTestUtil.XmlPullParserKind.class) + public void testUnparsedIq(SmackTestUtil.XmlPullParserKind parserKind) throws Exception { + final String iqXml = "" + + "" + + ""; + final String expected = "" + + "<query xmlns='jabber:iq:version'/>" + + ""; + + XmlPullParser parser = SmackTestUtil.getParserFor(iqXml, "iq", parserKind); + IQ iq = PacketParserUtils.parseIQ(parser); + assertXmlSimilar(expected, iq.toXML()); + } } diff --git a/smack-core/src/testFixtures/java/org/jivesoftware/smack/DummyConnection.java b/smack-core/src/testFixtures/java/org/jivesoftware/smack/DummyConnection.java index 7e105d58b..320bb1f9e 100644 --- a/smack-core/src/testFixtures/java/org/jivesoftware/smack/DummyConnection.java +++ b/smack-core/src/testFixtures/java/org/jivesoftware/smack/DummyConnection.java @@ -1,6 +1,6 @@ /** * - * Copyright 2010 Jive Software, 2022 Florian Schmaus. + * Copyright 2010 Jive Software, 2022-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. @@ -39,9 +39,9 @@ import org.jxmpp.stringprep.XmppStringprepException; * A dummy implementation of {@link XMPPConnection}, intended to be used during * unit tests. * - * Instances store any packets that are delivered to be send using the + * Instances store any packets that are delivered to be sent using the * {@link #sendStanza(Stanza)} method in a blocking queue. The content of this queue - * can be inspected using {@link #getSentPacket()}. Typically these queues are + * can be inspected using {@link #getSentPacket()}. Typically, these queues are * used to retrieve a message that was generated by the client. * * Packets that should be processed by the client to simulate a received stanza @@ -82,6 +82,7 @@ public class DummyConnection extends AbstractXMPPConnection { } } + @SuppressWarnings("this-escape") public DummyConnection(DummyConnectionConfiguration configuration) { super(configuration); @@ -91,6 +92,7 @@ public class DummyConnection extends AbstractXMPPConnection { user = getUserJid(); } + @SuppressWarnings("JavaUtilDate") @Override protected void connectInternal() { connected = true; @@ -162,6 +164,7 @@ public class DummyConnection extends AbstractXMPPConnection { * @param

      the top level stream element class. * @return a sent packet. */ + @SuppressWarnings("TypeParameterUnusedInFormals") public

      P getSentPacket() { return getSentPacket(5 * 60); } @@ -176,7 +179,7 @@ public class DummyConnection extends AbstractXMPPConnection { * @param

      the top level stream element class. * @return a sent packet. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) public

      P getSentPacket(int wait) { try { return (P) queue.poll(wait, TimeUnit.SECONDS); diff --git a/smack-core/src/testFixtures/java/org/jivesoftware/smack/test/util/MemoryLeakTestUtil.java b/smack-core/src/testFixtures/java/org/jivesoftware/smack/test/util/MemoryLeakTestUtil.java index 6746e8f13..1eccfd75a 100644 --- a/smack-core/src/testFixtures/java/org/jivesoftware/smack/test/util/MemoryLeakTestUtil.java +++ b/smack-core/src/testFixtures/java/org/jivesoftware/smack/test/util/MemoryLeakTestUtil.java @@ -41,7 +41,7 @@ import org.jxmpp.stringprep.XmppStringprepException; * Note that this test is based on the assumption that it is possible to trigger a full garbage collection run, which is * not the case. See also this * stackoverflow - * question. Hence the {@link #triggerGarbageCollection()} method defined in this class is not portable and depends + * question. Hence, the {@link #triggerGarbageCollection()} method defined in this class is not portable and depends * on implementation depended Java Virtual Machine behavior. *

      * diff --git a/smack-debug-slf4j/build.gradle b/smack-debug-slf4j/build.gradle index 9c2272729..71eae05b4 100644 --- a/smack-debug-slf4j/build.gradle +++ b/smack-debug-slf4j/build.gradle @@ -1,3 +1,7 @@ +plugins { + id 'org.igniterealtime.smack.java-common-conventions' +} + description = """\ Smack slf4j debugger. Inspect the exchanged XMPP stanzas. @@ -5,5 +9,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-debug-slf4j/src/main/java/org/jivesoftware/smackx/package-info.java b/smack-debug-slf4j/src/main/java/org/jivesoftware/smackx/package-info.java index 7ebe86244..bce3b6064 120000 --- a/smack-debug-slf4j/src/main/java/org/jivesoftware/smackx/package-info.java +++ b/smack-debug-slf4j/src/main/java/org/jivesoftware/smackx/package-info.java @@ -1 +1 @@ -../../../../../../../smack-java8-full/src/main/java/org/jivesoftware/smackx/package-info.java \ No newline at end of file +../../../../../../../smack-java11-full/src/main/java/org/jivesoftware/smackx/package-info.java \ No newline at end of file diff --git a/smack-debug/build.gradle b/smack-debug/build.gradle index 959caff4f..7c9fdc0f1 100644 --- a/smack-debug/build.gradle +++ b/smack-debug/build.gradle @@ -1,3 +1,7 @@ +plugins { + id 'org.igniterealtime.smack.java-common-conventions' +} + description = """\ Smack GUI debugger. Inspect the exchanged XMPP stanzas.""" diff --git a/smack-debug/src/main/java/org/jivesoftware/smackx/debugger/EnhancedDebugger.java b/smack-debug/src/main/java/org/jivesoftware/smackx/debugger/EnhancedDebugger.java index 40d2e5263..e57b4aaaa 100644 --- a/smack-debug/src/main/java/org/jivesoftware/smackx/debugger/EnhancedDebugger.java +++ b/smack-debug/src/main/java/org/jivesoftware/smackx/debugger/EnhancedDebugger.java @@ -160,6 +160,7 @@ public class EnhancedDebugger extends SmackDebugger { private ReaderListener readerListener; private WriterListener writerListener; + @SuppressWarnings("JavaUtilDate") private Date creationTime = new Date(); // Statistics variables @@ -177,6 +178,7 @@ public class EnhancedDebugger extends SmackDebugger { JTabbedPane tabbedPane; + @SuppressWarnings("this-escape") public EnhancedDebugger(XMPPConnection connection) { super(connection); @@ -756,6 +758,7 @@ public class EnhancedDebugger extends SmackDebugger { * @param dateFormatter the SimpleDateFormat to use to format Dates * @param packet the read stanza to add to the table */ + @SuppressWarnings("JavaUtilDate") private void addReadPacketToTable(final SimpleDateFormat dateFormatter, final TopLevelStreamElement packet) { SwingUtilities.invokeLater(new Runnable() { @Override @@ -827,6 +830,7 @@ public class EnhancedDebugger extends SmackDebugger { * @param dateFormatter the SimpleDateFormat to use to format Dates * @param packet the sent stanza to add to the table */ + @SuppressWarnings("JavaUtilDate") private void addSentPacketToTable(final SimpleDateFormat dateFormatter, final TopLevelStreamElement packet) { SwingUtilities.invokeLater(new Runnable() { @Override diff --git a/smack-debug/src/main/java/org/jivesoftware/smackx/debugger/EnhancedDebuggerWindow.java b/smack-debug/src/main/java/org/jivesoftware/smackx/debugger/EnhancedDebuggerWindow.java index 5755207d8..cadcb0b53 100644 --- a/smack-debug/src/main/java/org/jivesoftware/smackx/debugger/EnhancedDebuggerWindow.java +++ b/smack-debug/src/main/java/org/jivesoftware/smackx/debugger/EnhancedDebuggerWindow.java @@ -204,7 +204,7 @@ public final class EnhancedDebuggerWindow { * Creates the main debug window that provides information about Smack and also shows * a tab panel for each connection that is being debugged. */ - @SuppressWarnings({ "rawtypes", "unchecked" }) + @SuppressWarnings({ "rawtypes", "unchecked", "JdkObsolete" }) private void createDebug() { frame = new JFrame("Smack Debug Window"); diff --git a/smack-debug/src/main/java/org/jivesoftware/smackx/package-info.java b/smack-debug/src/main/java/org/jivesoftware/smackx/package-info.java index 7ebe86244..bce3b6064 120000 --- a/smack-debug/src/main/java/org/jivesoftware/smackx/package-info.java +++ b/smack-debug/src/main/java/org/jivesoftware/smackx/package-info.java @@ -1 +1 @@ -../../../../../../../smack-java8-full/src/main/java/org/jivesoftware/smackx/package-info.java \ No newline at end of file +../../../../../../../smack-java11-full/src/main/java/org/jivesoftware/smackx/package-info.java \ No newline at end of file diff --git a/smack-examples/build.gradle b/smack-examples/build.gradle new file mode 100644 index 000000000..244040dd7 --- /dev/null +++ b/smack-examples/build.gradle @@ -0,0 +1,14 @@ +plugins { + id 'org.igniterealtime.smack.java-common-conventions' +} + +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 97% 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..9c8652107 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; @@ -185,7 +185,7 @@ public class OmemoClient { BareJid contact = JidCreate.bareFrom(com[1]); - HashMap devices; + Map devices; try { devices = omemoManager.getActiveFingerprints(contact); } catch (CorruptedOmemoKeyException | CannotEstablishOmemoSessionException | SmackException.NoResponseException e) { 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-examples/src/main/java/org/igniterealtime/smack/examples/XmppConnectionTool.java b/smack-examples/src/main/java/org/igniterealtime/smack/examples/XmppConnectionTool.java new file mode 100644 index 000000000..c44adb157 --- /dev/null +++ b/smack-examples/src/main/java/org/igniterealtime/smack/examples/XmppConnectionTool.java @@ -0,0 +1,82 @@ +/** + * + * Copyright 2024 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.io.IOException; + +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.SmackException.NoResponseException; +import org.jivesoftware.smack.SmackException.NotConnectedException; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.XMPPException.XMPPErrorException; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionConfiguration; +import org.jivesoftware.smack.debugger.ConsoleDebugger; +import org.jivesoftware.smack.debugger.SmackDebuggerFactory; +import org.jivesoftware.smackx.omemo.util.OmemoConstants; +import org.jivesoftware.smackx.pep.PepManager; +import org.jivesoftware.smackx.pubsub.PubSubManager; + +import org.jxmpp.stringprep.XmppStringprepException; + +public class XmppConnectionTool { + + public final ModularXmppClientToServerConnection connection; + + public XmppConnectionTool(ModularXmppClientToServerConnection connection) { + this.connection = connection; + } + + public boolean purgeOmemoInformation() + throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { + PepManager pepManager = PepManager.getInstanceFor(connection); + PubSubManager pepPubSubManager = pepManager.getPepPubSubManager(); + + // TODO: Also delete "bundles" nodes. + return pepPubSubManager.deleteNode(OmemoConstants.PEP_NODE_DEVICE_LIST); + } + + public static XmppConnectionTool of(String jid, String password, boolean debug) + throws XMPPException, SmackException, IOException, InterruptedException { + ModularXmppClientToServerConnection connection = createConnectionFor(jid, password, debug); + connection.connect().login(); + return new XmppConnectionTool(connection); + } + + public static ModularXmppClientToServerConnection createConnectionFor(String jid, String password, boolean debug) + throws XmppStringprepException { + + final SmackDebuggerFactory smackDebuggerFactory; + if (debug) { + smackDebuggerFactory = ConsoleDebugger.Factory.INSTANCE; + } else { + smackDebuggerFactory = null; + } + + ModularXmppClientToServerConnectionConfiguration.Builder configurationBuilder = ModularXmppClientToServerConnectionConfiguration + .builder().setXmppAddressAndPassword(jid, password).setDebuggerFactory(smackDebuggerFactory); + + ModularXmppClientToServerConnectionConfiguration configuration = configurationBuilder.build(); + + ModularXmppClientToServerConnection connection = new ModularXmppClientToServerConnection(configuration); + return connection; + } +} 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 96% 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..70bb9f1f2 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 @@ -1,10 +1,10 @@ /** * - * Copyright 2016-2021 Florian Schmaus + * Copyright 2016-2024 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; @@ -136,6 +136,7 @@ public class XmppTools { // CHECKSTYLE:ON } + @SuppressWarnings("JavaUtilDate") public static void sendItsAlive(String to, XMPPConnection connection) throws XmppStringprepException, NotConnectedException, InterruptedException { if (to == null) { 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/build.gradle b/smack-experimental/build.gradle index 9e945e0d5..7484d56f8 100644 --- a/smack-experimental/build.gradle +++ b/smack-experimental/build.gradle @@ -1,3 +1,8 @@ +plugins { + id 'org.igniterealtime.smack.java-common-conventions' + id 'org.igniterealtime.smack.android-conventions' +} + description = """\ Smack experimental extensions. Classes and methods for XEPs that are in status 'experimental' or that should diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/carbons/packet/Carbon.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/carbons/packet/Carbon.java index 8f303cdff..77ef518f6 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/carbons/packet/Carbon.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/carbons/packet/Carbon.java @@ -1,6 +1,6 @@ /** * - * Copyright © 2014 Florian Schmaus + * Copyright © 2014-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. @@ -27,6 +27,7 @@ public class Carbon { public static class Enable extends SimpleIQ { public static final String ELEMENT = "enable"; + @SuppressWarnings("this-escape") public Enable() { super(ELEMENT, NAMESPACE); setType(Type.set); @@ -37,6 +38,7 @@ public class Carbon { public static class Disable extends SimpleIQ { public static final String ELEMENT = "disable"; + @SuppressWarnings("this-escape") public Disable() { super(ELEMENT, NAMESPACE); setType(Type.set); 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/dox/element/DnsIq.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/dox/element/DnsIq.java index 77c626eae..590906549 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/dox/element/DnsIq.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/dox/element/DnsIq.java @@ -1,6 +1,6 @@ /** * - * Copyright 2019 Florian Schmaus + * Copyright 2019-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. @@ -43,6 +43,7 @@ public class DnsIq extends IQ { this(new DnsMessage(dnsMessage)); } + @SuppressWarnings("this-escape") public DnsIq(DnsMessage dnsQuery, Jid to) { this(dnsQuery); setTo(to); diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/file_metadata/element/FileMetadataElement.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/file_metadata/element/FileMetadataElement.java new file mode 100644 index 000000000..e9f2976bb --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/file_metadata/element/FileMetadataElement.java @@ -0,0 +1,312 @@ +/** + * + * 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.file_metadata.element; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.jivesoftware.smack.packet.ExtensionElement; +import org.jivesoftware.smack.packet.XmlEnvironment; +import org.jivesoftware.smack.util.CollectionUtil; +import org.jivesoftware.smack.util.EqualsUtil; +import org.jivesoftware.smack.util.HashCode; +import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smack.util.XmlStringBuilder; +import org.jivesoftware.smackx.hashes.HashManager; +import org.jivesoftware.smackx.hashes.element.HashElement; +import org.jivesoftware.smackx.thumbnails.element.ThumbnailElement; + +/** + * File metadata element as defined in XEP-0446: File Metadata Element. + * This element is used in a generic way to provide information about files, e.g. during file sharing. + */ +public final class FileMetadataElement implements ExtensionElement { + + public static final String ELEMENT = "file"; + public static final String NAMESPACE = "urn:xmpp:file:metadata:0"; + public static final String ELEM_DATE = "date"; + public static final String ELEM_HEIGHT = "height"; + public static final String ELEM_WIDTH = "width"; + public static final String ELEM_DESC = "desc"; + public static final String ELEM_LENGTH = "length"; + public static final String ELEM_MEDIA_TYPE = "media-type"; + public static final String ELEM_NAME = "name"; + public static final String ELEM_SIZE = "size"; + + + private final Date date; + private final Integer height; + private final Integer width; + private final Map descriptions; + private final Map hashElements; + private final Long length; + private final String mediaType; + private final String name; + private final Long size; + private final List thumbnails; + + private FileMetadataElement(Date date, Integer height, Integer width, Map descriptions, + Map hashElements, Long length, + String mediaType, String name, Long size, + List thumbnails) { + this.date = date; + this.height = height; + this.width = width; + this.descriptions = CollectionUtil.cloneAndSeal(descriptions); + this.hashElements = CollectionUtil.cloneAndSeal(hashElements); + this.length = length; + this.mediaType = mediaType; + this.name = name; + this.size = size; + this.thumbnails = CollectionUtil.cloneAndSeal(thumbnails); + } + + @Override + public XmlStringBuilder toXML(XmlEnvironment xmlEnvironment) { + XmlStringBuilder sb = new XmlStringBuilder(this) + .rightAngleBracket() + .optElement(ELEM_DATE, date) + .optElement(ELEM_HEIGHT, height) + .optElement(ELEM_WIDTH, width); + for (String key : descriptions.keySet()) { + sb.halfOpenElement(ELEM_DESC) + .optXmlLangAttribute(key) + .rightAngleBracket() + .append(descriptions.get(key)) + .closeElement(ELEM_DESC); + } + sb.append(hashElements.values()) + .optElement(ELEM_LENGTH, length != null ? Long.toString(length) : null) + .optElement(ELEM_MEDIA_TYPE, mediaType) + .optElement(ELEM_NAME, name) + .optElement(ELEM_SIZE, size != null ? Long.toString(size) : null) + .append(thumbnails); + return sb.closeElement(this); + } + + @Override + public String getNamespace() { + return NAMESPACE; + } + + @Override + public String getElementName() { + return ELEMENT; + } + + public Date getDate() { + return date; + } + + public Integer getHeight() { + return height; + } + + public Integer getWidth() { + return width; + } + + public Map getDescriptions() { + return Collections.unmodifiableMap(descriptions); + } + + public String getDescription() { + return getDescription(getLanguage()); + } + + public String getDescription(String lang) { + return descriptions.get(lang != null ? lang : ""); + } + + public Map getHashElements() { + return Collections.unmodifiableMap(hashElements); + } + + public HashElement getHashElement(HashManager.ALGORITHM algorithm) { + return hashElements.get(algorithm); + } + + public Long getLength() { + return length; + } + + public String getMediaType() { + return mediaType; + } + + /** + * Return the name of the file. + * + * @return escaped name + */ + public String getName() { + if (name == null) { + return null; + } + try { + return URLEncoder.encode(name, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new AssertionError(e); // UTF-8 MUST be supported + } + } + + public String getRawName() { + return name; + } + + public Long getSize() { + return size; + } + + public List getThumbnails() { + return Collections.unmodifiableList(thumbnails); + } + + @Override + public int hashCode() { + return HashCode.builder() + .append(getElementName()) + .append(getNamespace()) + .append(getDate()) + .append(getDescriptions()) + .append(getHeight()) + .append(getWidth()) + .append(getHashElements()) + .append(getLength()) + .append(getMediaType()) + .append(getRawName()) + .append(getSize()) + .append(getThumbnails()) + .build(); + } + + @Override + public boolean equals(Object other) { + return EqualsUtil.equals(this, other, (equalsBuilder, o) -> equalsBuilder + .append(getElementName(), o.getElementName()) + .append(getNamespace(), o.getNamespace()) + .append(getDate(), o.getDate()) + .append(getDescriptions(), o.getDescriptions()) + .append(getHeight(), o.getHeight()) + .append(getWidth(), o.getWidth()) + .append(getHashElements(), o.getHashElements()) + .append(getLength(), o.getLength()) + .append(getMediaType(), o.getMediaType()) + .append(getRawName(), o.getRawName()) + .append(getSize(), o.getSize()) + .append(getThumbnails(), o.getThumbnails())); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private Date date; + private Integer height; + private Integer width; + private Map descriptions = new HashMap<>(); + private Map hashElements = new HashMap<>(); + private Long length; + private String mediaType; + private String name; + private Long size; + private List thumbnails = new ArrayList<>(); + + public Builder setModificationDate(Date date) { + this.date = date; + return this; + } + + public Builder setDimensions(int width, int height) { + return setHeight(height).setWidth(width); + } + + public Builder setHeight(int height) { + if (height <= 0) { + throw new IllegalArgumentException("Height must be a positive number"); + } + this.height = height; + return this; + } + + public Builder setWidth(int width) { + if (width <= 0) { + throw new IllegalArgumentException("Width must be a positive number"); + } + this.width = width; + return this; + } + + public Builder addDescription(String description) { + return addDescription(description, null); + } + + public Builder addDescription(String description, String language) { + this.descriptions.put(language != null ? language : "", StringUtils.requireNotNullNorEmpty(description, "Description MUST NOT be null nor empty")); + return this; + } + + public Builder addHash(HashElement hashElement) { + hashElements.put(hashElement.getAlgorithm(), hashElement); + return this; + } + + public Builder setLength(long length) { + if (length < 0) { + throw new IllegalArgumentException("Length cannot be negative."); + } + this.length = length; + return this; + } + + public Builder setMediaType(String mediaType) { + this.mediaType = StringUtils.requireNotNullNorEmpty(mediaType, "Media-Type MUST NOT be null nor empty"); + return this; + } + + public Builder setName(String name) { + this.name = StringUtils.requireNotNullNorEmpty(name, "Name MUST NOT be null nor empty"); + return this; + } + + public Builder setSize(long size) { + if (size < 0) { + throw new IllegalArgumentException("Size MUST NOT be negative."); + } + this.size = size; + return this; + } + + public Builder addThumbnail(ThumbnailElement thumbnail) { + thumbnails.add(thumbnail); + return this; + } + + public FileMetadataElement build() { + return new FileMetadataElement(date, height, width, descriptions, hashElements, length, + mediaType, name, size, thumbnails); + } + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/file_metadata/element/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/file_metadata/element/package-info.java new file mode 100644 index 000000000..dbea97273 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/file_metadata/element/package-info.java @@ -0,0 +1,20 @@ +/** + * + * 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. + */ +/** + * Smacks implementation of XEP-0446: File Metadata Element. + */ +package org.jivesoftware.smackx.file_metadata.element; diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/file_metadata/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/file_metadata/package-info.java new file mode 100644 index 000000000..8d9825d6c --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/file_metadata/package-info.java @@ -0,0 +1,20 @@ +/** + * + * 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. + */ +/** + * Smacks implementation of XEP-0446: File Metadata Element. + */ +package org.jivesoftware.smackx.file_metadata; diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/file_metadata/provider/FileMetadataElementProvider.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/file_metadata/provider/FileMetadataElementProvider.java new file mode 100644 index 000000000..e4212b0ce --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/file_metadata/provider/FileMetadataElementProvider.java @@ -0,0 +1,102 @@ +/** + * + * 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.file_metadata.provider; + +import java.io.IOException; +import java.text.ParseException; + +import org.jivesoftware.smack.packet.XmlEnvironment; +import org.jivesoftware.smack.parsing.SmackParsingException; +import org.jivesoftware.smack.provider.ExtensionElementProvider; +import org.jivesoftware.smack.util.ParserUtils; +import org.jivesoftware.smack.xml.XmlPullParser; +import org.jivesoftware.smack.xml.XmlPullParserException; +import org.jivesoftware.smackx.file_metadata.element.FileMetadataElement; +import org.jivesoftware.smackx.hashes.element.HashElement; +import org.jivesoftware.smackx.hashes.provider.HashElementProvider; +import org.jivesoftware.smackx.thumbnails.element.ThumbnailElement; +import org.jivesoftware.smackx.thumbnails.provider.ThumbnailElementProvider; + +public class FileMetadataElementProvider extends ExtensionElementProvider { + + @Override + public FileMetadataElement parse(XmlPullParser parser, int initialDepth, XmlEnvironment xmlEnvironment) + throws XmlPullParserException, IOException, SmackParsingException, ParseException { + FileMetadataElement.Builder builder = FileMetadataElement.builder(); + + outerloop: while (true) { + XmlPullParser.Event event = parser.next(); + switch (event) { + case START_ELEMENT: + String name = parser.getName(); + switch (name) { + case FileMetadataElement.ELEMENT: + parser.next(); + break; + case FileMetadataElement.ELEM_DATE: + builder.setModificationDate(ParserUtils.getDateFromNextText(parser)); + break; + case FileMetadataElement.ELEM_DESC: + String lang = ParserUtils.getXmlLang(parser); + builder.addDescription(ParserUtils.getRequiredNextText(parser), lang); + break; + case "dimensions": // was replaced with width and height + String dimensions = ParserUtils.getRequiredNextText(parser); + String[] split = dimensions.split("x"); + if (split.length != 2) { + throw new IllegalArgumentException("Invalid dimensions."); + } + builder.setWidth(Integer.parseInt(split[0])); + builder.setHeight(Integer.parseInt(split[1])); + break; + case FileMetadataElement.ELEM_WIDTH: + builder.setWidth(Integer.parseInt(ParserUtils.getRequiredNextText(parser))); + break; + case FileMetadataElement.ELEM_HEIGHT: + builder.setHeight(Integer.parseInt(ParserUtils.getRequiredNextText(parser))); + break; + case FileMetadataElement.ELEM_LENGTH: + builder.setLength(Long.parseLong(ParserUtils.getRequiredNextText(parser))); + break; + case FileMetadataElement.ELEM_MEDIA_TYPE: + builder.setMediaType(ParserUtils.getRequiredNextText(parser)); + break; + case FileMetadataElement.ELEM_NAME: + builder.setName(ParserUtils.getRequiredNextText(parser)); + break; + case FileMetadataElement.ELEM_SIZE: + builder.setSize(Long.parseLong(ParserUtils.getRequiredNextText(parser))); + break; + case HashElement.ELEMENT: + builder.addHash(HashElementProvider.INSTANCE.parse(parser, parser.getDepth(), xmlEnvironment)); + break; + case ThumbnailElement.ELEMENT: + ThumbnailElementProvider provider = new ThumbnailElementProvider(); + builder.addThumbnail(provider.parse(parser, parser.getDepth(), xmlEnvironment)); + } + break; + case END_ELEMENT: + if (parser.getDepth() == initialDepth) break outerloop; + break; + default: + // Catch all for incomplete switch (MissingCasesInEnumSwitch) statement. + break; + } + } + return builder.build(); + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/file_metadata/provider/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/file_metadata/provider/package-info.java new file mode 100644 index 000000000..e28f0d94b --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/file_metadata/provider/package-info.java @@ -0,0 +1,20 @@ +/** + * + * 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. + */ +/** + * File metadata element provider. + */ +package org.jivesoftware.smackx.file_metadata.provider; diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/hashes/HashManager.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/hashes/HashManager.java index b3f0c3db4..68aa79ba7 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/hashes/HashManager.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/hashes/HashManager.java @@ -47,7 +47,7 @@ import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; import org.jivesoftware.smackx.hashes.element.HashElement; /** - * Manager that can be used to determine support for hash functions. By default the Manager announces support for + * Manager that can be used to determine support for hash functions. By default,the Manager announces support for * XEP-0300, as well as for the recommended set of hash algorithms. Those contain SHA256, SHA384, SHA512, SHA3-256, * SHA3-384, SHA3-512, BLAKE2B256, BLAKE2B384 and BLAKE2B512. Those algorithms got recommended here: * https://xmpp.org/extensions/xep-0300.html#recommendations. diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/hoxt/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/hoxt/package-info.java index 1b357afa8..9ca583637 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/hoxt/package-info.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/hoxt/package-info.java @@ -103,7 +103,7 @@ * AbstractHttpOverXmpp.Data data = new AbstractHttpOverXmpp.Data(child); * * // create request - * HttpOverXmppReq req = HttpOverXmppReq.buider() + * HttpOverXmppReq req = HttpOverXmppReq.builder() * .setMethod(HttpMethod.POST) * .setResource("/mailbox") * .setHeaders(headers) diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/Slot.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/Slot.java index 274e59bc0..48c61bec7 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/Slot.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/Slot.java @@ -46,6 +46,7 @@ public class Slot extends IQ { this(putUrl, getUrl, headers, NAMESPACE); } + @SuppressWarnings("this-escape") protected Slot(URL putUrl, URL getUrl, Map headers, String namespace) { super(ELEMENT, namespace); setType(Type.result); diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/SlotRequest.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/SlotRequest.java index 961fc27e1..0fa8b484c 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/SlotRequest.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/SlotRequest.java @@ -53,6 +53,7 @@ public class SlotRequest extends IQ { this(uploadServiceAddress, filename, size, contentType, NAMESPACE); } + @SuppressWarnings("this-escape") protected SlotRequest(DomainBareJid uploadServiceAddress, String filename, long size, String contentType, String namespace) { super(ELEMENT, namespace); diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/control/element/IoTSetRequest.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/control/element/IoTSetRequest.java index cd6d0b553..a3dad0268 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/control/element/IoTSetRequest.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/control/element/IoTSetRequest.java @@ -1,6 +1,6 @@ /** * - * Copyright © 2016-2017 Florian Schmaus + * Copyright © 2016-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. @@ -29,6 +29,7 @@ public class IoTSetRequest extends IQ { private final Collection setData; + @SuppressWarnings("this-escape") public IoTSetRequest(Collection setData) { super(ELEMENT, NAMESPACE); setType(Type.set); diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/data/element/IoTDataReadOutAccepted.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/data/element/IoTDataReadOutAccepted.java index 20d0a125d..279476cd2 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/data/element/IoTDataReadOutAccepted.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/data/element/IoTDataReadOutAccepted.java @@ -1,6 +1,6 @@ /** * - * Copyright © 2016 Florian Schmaus + * Copyright © 2016-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. @@ -30,6 +30,7 @@ public class IoTDataReadOutAccepted extends IQ { private final boolean queued; + @SuppressWarnings("this-escape") public IoTDataReadOutAccepted(int seqNr, boolean queued) { super(ELEMENT, NAMESPACE); this.seqNr = seqNr; @@ -37,6 +38,7 @@ public class IoTDataReadOutAccepted extends IQ { setType(Type.result); } + @SuppressWarnings("this-escape") public IoTDataReadOutAccepted(IoTDataRequest dataRequest) { this(dataRequest.getSequenceNr(), false); setStanzaId(dataRequest.getStanzaId()); diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/data/element/IoTFieldsExtension.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/data/element/IoTFieldsExtension.java index cb280745f..721b1b657 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/data/element/IoTFieldsExtension.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/data/element/IoTFieldsExtension.java @@ -83,6 +83,7 @@ public class IoTFieldsExtension implements ExtensionElement { return xml; } + @SuppressWarnings("JavaUtilDate") public static IoTFieldsExtension buildFor(int seqNr, boolean done, NodeInfo nodeInfo, List data) { TimestampElement timestampElement = new TimestampElement(new Date(), data); diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/discovery/element/IoTUnregister.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/discovery/element/IoTUnregister.java index bdc536f97..3b510e55f 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/discovery/element/IoTUnregister.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/discovery/element/IoTUnregister.java @@ -1,6 +1,6 @@ /** * - * Copyright 2016 Florian Schmaus + * Copyright 2016-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. @@ -27,6 +27,7 @@ public class IoTUnregister extends IQ { private final NodeInfo nodeInfo; + @SuppressWarnings("this-escape") public IoTUnregister(NodeInfo nodeInfo) { super(ELEMENT, NAMESPACE); this.nodeInfo = nodeInfo; diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/package-info.java index 0b3803a4f..c4b261a27 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/package-info.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/package-info.java @@ -146,7 +146,7 @@ *
        * 
        * IoTDiscoveryManager iotDiscoveryManager = IoTDiscoveryManager.getInstanceFor(connection);
      - * iotDiscovyerManager.registerThing(thing);
      + * iotDiscoveryManager.registerThing(thing);
        * 
        * 
      *

      @@ -162,7 +162,7 @@ * Things can usually only be used by other things if they are friends. Since a * thing normally can't decide on its own if an incoming friendship request * should be granted or not, we can delegate this decision to a provisioning - * service. Smack provides the `IoTProvisinoManager` to deal with friendship and + * service. Smack provides the `IoTProvisionManager` to deal with friendship and * provisioning. *

      *

      diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/element/ClearCache.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/element/ClearCache.java index fe98b32b3..d17e46c94 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/element/ClearCache.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/element/ClearCache.java @@ -1,6 +1,6 @@ /** * - * Copyright © 2016 Florian Schmaus + * Copyright © 2016-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. @@ -23,6 +23,7 @@ public class ClearCache extends SimpleIQ { public static final String ELEMENT = "clearCache"; public static final String NAMESPACE = Constants.IOT_PROVISIONING_NAMESPACE; + @SuppressWarnings("this-escape") public ClearCache() { super(ELEMENT, NAMESPACE); // IQs are always of type 'get' (XEP-0324 § 3.5.1, see also the XEPs history remarks) diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/element/ClearCacheResponse.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/element/ClearCacheResponse.java index 1c229d153..1abfb731c 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/element/ClearCacheResponse.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/element/ClearCacheResponse.java @@ -1,6 +1,6 @@ /** * - * Copyright © 2016 Florian Schmaus + * Copyright © 2016-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. @@ -23,12 +23,14 @@ public class ClearCacheResponse extends SimpleIQ { public static final String ELEMENT = "clearCacheResponse"; public static final String NAMESPACE = Constants.IOT_PROVISIONING_NAMESPACE; + @SuppressWarnings("this-escape") public ClearCacheResponse() { super(ELEMENT, NAMESPACE); // IQs are always of type 'result' (XEP-0324 § 3.5.1, see also the XEPs history remarks) setType(Type.result); } + @SuppressWarnings("this-escape") public ClearCacheResponse(ClearCache clearCacheRequest) { this(); setStanzaId(clearCacheRequest.getStanzaId()); diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/jingle_filetransfer/element/JingleFileTransferChild.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/jingle_filetransfer/element/JingleFileTransferChild.java index b4e9021ec..cab86e550 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/jingle_filetransfer/element/JingleFileTransferChild.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/jingle_filetransfer/element/JingleFileTransferChild.java @@ -1,6 +1,6 @@ /** * - * Copyright 2017 Paul Schaub, 2019 Florian Schmaus + * Copyright 2017 Paul Schaub, 2019-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. @@ -167,6 +167,7 @@ public class JingleFileTransferChild implements JingleContentDescriptionChildEle return new JingleFileTransferChild(date, desc, hash, mediaType, name, size, range); } + @SuppressWarnings("JavaUtilDate") public Builder setFile(File file) { return setDate(new Date(file.lastModified())) .setName(file.getAbsolutePath().substring(file.getAbsolutePath().lastIndexOf("/") + 1)) 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..6ce58bdea 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-2024 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); } /** @@ -387,6 +387,7 @@ public final class MamManager extends Manager { return this; } + @SuppressWarnings("JavaUtilDate") public Builder limitResultsSince(Date start) { if (start == null) { return this; @@ -415,6 +416,7 @@ public final class MamManager extends Manager { return this; } + @SuppressWarnings("JavaUtilDate") public Builder limitResultsBefore(Date end) { if (end == null) { return this; @@ -759,7 +761,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-experimental/src/main/java/org/jivesoftware/smackx/mam/element/MamPrefsIQ.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/element/MamPrefsIQ.java index e5898f4c3..1a5535135 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/element/MamPrefsIQ.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/element/MamPrefsIQ.java @@ -81,6 +81,7 @@ public class MamPrefsIQ extends IQ { * @param neverJids TODO javadoc me please * @param defaultBehavior TODO javadoc me please */ + @SuppressWarnings("this-escape") public MamPrefsIQ(MamVersion version, List alwaysJids, List neverJids, DefaultBehavior defaultBehavior) { super(ELEMENT, version.getNamespace()); setType(Type.set); diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/element/MamQueryIQ.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/element/MamQueryIQ.java index 3909b077c..17b8a5cae 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/element/MamQueryIQ.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/element/MamQueryIQ.java @@ -45,6 +45,7 @@ public class MamQueryIQ extends IQ { * @param version TODO javadoc me please * @param queryId TODO javadoc me please */ + @SuppressWarnings("this-escape") public MamQueryIQ(MamVersion version, String queryId) { this(version, queryId, null, null); setType(IQ.Type.get); @@ -79,6 +80,7 @@ public class MamQueryIQ extends IQ { * @param node TODO javadoc me please * @param dataForm TODO javadoc me please */ + @SuppressWarnings("this-escape") public MamQueryIQ(MamVersion version, String queryId, String node, DataForm dataForm) { super(ELEMENT, version.getNamespace()); this.queryId = queryId; diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/message_markup/element/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_markup/element/package-info.java index 5246c33ff..f1d6e981e 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/message_markup/element/package-info.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_markup/element/package-info.java @@ -21,7 +21,7 @@ *

      Usage

      *

      * The most important class is the {@link org.jivesoftware.smackx.message_markup.element.MarkupElement} class, which - * contains a Builder to construct message markup.. + * contains a Builder to construct message markup. *

      *

      * To start creating a Message Markup Extension, call diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/MUCLightRoomConfiguration.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/MUCLightRoomConfiguration.java index aab822087..7f0595871 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/MUCLightRoomConfiguration.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/MUCLightRoomConfiguration.java @@ -16,7 +16,7 @@ */ package org.jivesoftware.smackx.muclight; -import java.util.HashMap; +import java.util.Map; /** * MUC Light room configuration class. @@ -28,7 +28,7 @@ public class MUCLightRoomConfiguration { private final String roomName; private final String subject; - private final HashMap customConfigs; + private final Map customConfigs; /** * MUC Light room configuration model constructor. @@ -37,7 +37,7 @@ public class MUCLightRoomConfiguration { * @param subject TODO javadoc me please * @param customConfigs TODO javadoc me please */ - public MUCLightRoomConfiguration(String roomName, String subject, HashMap customConfigs) { + public MUCLightRoomConfiguration(String roomName, String subject, Map customConfigs) { this.roomName = roomName; this.subject = subject; this.customConfigs = customConfigs; @@ -66,7 +66,7 @@ public class MUCLightRoomConfiguration { * * @return the custom configurations of the room. */ - public HashMap getCustomConfigs() { + public Map getCustomConfigs() { return customConfigs; } diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/MUCLightRoomInfo.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/MUCLightRoomInfo.java index b41066d54..532f64bf4 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/MUCLightRoomInfo.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/MUCLightRoomInfo.java @@ -16,7 +16,7 @@ */ package org.jivesoftware.smackx.muclight; -import java.util.HashMap; +import java.util.Map; import org.jxmpp.jid.Jid; @@ -30,7 +30,7 @@ public class MUCLightRoomInfo { private final String version; private final Jid room; private final MUCLightRoomConfiguration configuration; - private final HashMap occupants; + private final Map occupants; /** * MUC Light room info model constructor. @@ -41,7 +41,7 @@ public class MUCLightRoomInfo { * @param occupants TODO javadoc me please */ public MUCLightRoomInfo(String version, Jid roomJid, MUCLightRoomConfiguration configuration, - HashMap occupants) { + Map occupants) { this.version = version; this.room = roomJid; this.configuration = configuration; @@ -80,7 +80,7 @@ public class MUCLightRoomInfo { * * @return the occupants of the room. */ - public HashMap getOccupants() { + public Map getOccupants() { return occupants; } diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/MultiUserChatLight.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/MultiUserChatLight.java index 5ebd11197..58e5b367d 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/MultiUserChatLight.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/MultiUserChatLight.java @@ -18,6 +18,7 @@ package org.jivesoftware.smackx.muclight; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; @@ -181,21 +182,6 @@ public class MultiUserChatLight { ; } - /** - * Sends a Message to the chat room. - * - * @param message TODO javadoc me please - * the message. - * @throws NotConnectedException if the XMPP connection is not connected. - * @throws InterruptedException if the calling thread was interrupted. - * @deprecated use {@link #sendMessage(MessageBuilder)} instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public void sendMessage(Message message) throws NotConnectedException, InterruptedException { - sendMessage(message.asBuilder()); - } - /** * Sends a Message to the chat room. * @@ -294,7 +280,7 @@ public class MultiUserChatLight { * @param occupants TODO javadoc me please * @throws Exception TODO javadoc me please */ - public void create(String roomName, String subject, HashMap customConfigs, List occupants) + public void create(String roomName, String subject, Map customConfigs, List occupants) throws Exception { MUCLightCreateIQ createMUCLightIQ = new MUCLightCreateIQ(room, roomName, occupants); @@ -328,7 +314,7 @@ public class MultiUserChatLight { * @throws XMPPErrorException if there was an XMPP error returned. */ public void leave() throws NotConnectedException, InterruptedException, NoResponseException, XMPPErrorException { - HashMap affiliations = new HashMap<>(); + Map affiliations = new HashMap<>(); affiliations.put(connection.getUser(), MUCLightAffiliation.none); MUCLightChangeAffiliationsIQ changeAffiliationsIQ = new MUCLightChangeAffiliationsIQ(room, affiliations); @@ -417,7 +403,7 @@ public class MultiUserChatLight { * @throws NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. */ - public HashMap getAffiliations(String version) + public Map getAffiliations(String version) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { MUCLightGetAffiliationsIQ mucLightGetAffiliationsIQ = new MUCLightGetAffiliationsIQ(room, version); @@ -436,7 +422,7 @@ public class MultiUserChatLight { * @throws NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. */ - public HashMap getAffiliations() + public Map getAffiliations() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { return getAffiliations(null); } @@ -450,7 +436,7 @@ public class MultiUserChatLight { * @throws NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. */ - public void changeAffiliations(HashMap affiliations) + public void changeAffiliations(Map affiliations) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { MUCLightChangeAffiliationsIQ changeAffiliationsIQ = new MUCLightChangeAffiliationsIQ(room, affiliations); connection.sendIqRequestAndWaitForResponse(changeAffiliationsIQ); @@ -513,7 +499,7 @@ public class MultiUserChatLight { * @throws NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. */ - public void setRoomConfigs(HashMap customConfigs) + public void setRoomConfigs(Map customConfigs) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { setRoomConfigs(null, customConfigs); } @@ -528,7 +514,7 @@ public class MultiUserChatLight { * @throws NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. */ - public void setRoomConfigs(String roomName, HashMap customConfigs) + public void setRoomConfigs(String roomName, Map customConfigs) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { MUCLightSetConfigsIQ mucLightSetConfigIQ = new MUCLightSetConfigsIQ(room, roomName, customConfigs); connection.sendIqRequestAndWaitForResponse(mucLightSetConfigIQ); diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/MultiUserChatLightManager.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/MultiUserChatLightManager.java index d153d499d..58f030499 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/MultiUserChatLightManager.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/MultiUserChatLightManager.java @@ -274,7 +274,7 @@ public final class MultiUserChatLightManager extends Manager { sendBlockRooms(mucLightService, rooms); } - private void sendBlockRooms(DomainBareJid mucLightService, HashMap rooms) + private void sendBlockRooms(DomainBareJid mucLightService, Map rooms) throws NoResponseException, XMPPErrorException, InterruptedException, NotConnectedException { MUCLightBlockingIQ mucLightBlockingIQ = new MUCLightBlockingIQ(rooms, null); mucLightBlockingIQ.setType(IQ.Type.set); @@ -318,7 +318,7 @@ public final class MultiUserChatLightManager extends Manager { sendBlockUsers(mucLightService, users); } - private void sendBlockUsers(DomainBareJid mucLightService, HashMap users) + private void sendBlockUsers(DomainBareJid mucLightService, Map users) throws NoResponseException, XMPPErrorException, InterruptedException, NotConnectedException { MUCLightBlockingIQ mucLightBlockingIQ = new MUCLightBlockingIQ(null, users); mucLightBlockingIQ.setType(IQ.Type.set); @@ -362,7 +362,7 @@ public final class MultiUserChatLightManager extends Manager { sendUnblockRooms(mucLightService, rooms); } - private void sendUnblockRooms(DomainBareJid mucLightService, HashMap rooms) + private void sendUnblockRooms(DomainBareJid mucLightService, Map rooms) throws NoResponseException, XMPPErrorException, InterruptedException, NotConnectedException { MUCLightBlockingIQ mucLightBlockingIQ = new MUCLightBlockingIQ(rooms, null); mucLightBlockingIQ.setType(IQ.Type.set); @@ -406,7 +406,7 @@ public final class MultiUserChatLightManager extends Manager { sendUnblockUsers(mucLightService, users); } - private void sendUnblockUsers(DomainBareJid mucLightService, HashMap users) + private void sendUnblockUsers(DomainBareJid mucLightService, Map users) throws NoResponseException, XMPPErrorException, InterruptedException, NotConnectedException { MUCLightBlockingIQ mucLightBlockingIQ = new MUCLightBlockingIQ(null, users); mucLightBlockingIQ.setType(IQ.Type.set); diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightAffiliationsIQ.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightAffiliationsIQ.java index c9e0ead62..047c7af50 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightAffiliationsIQ.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightAffiliationsIQ.java @@ -16,7 +16,6 @@ */ package org.jivesoftware.smackx.muclight.element; -import java.util.HashMap; import java.util.Iterator; import java.util.Map; @@ -40,7 +39,7 @@ public class MUCLightAffiliationsIQ extends IQ { public static final String NAMESPACE = MultiUserChatLight.NAMESPACE + MultiUserChatLight.AFFILIATIONS; private final String version; - private HashMap affiliations; + private Map affiliations; /** * MUC Light affiliations response IQ constructor. @@ -48,7 +47,8 @@ public class MUCLightAffiliationsIQ extends IQ { * @param version TODO javadoc me please * @param affiliations TODO javadoc me please */ - public MUCLightAffiliationsIQ(String version, HashMap affiliations) { + @SuppressWarnings("this-escape") + public MUCLightAffiliationsIQ(String version, Map affiliations) { super(ELEMENT, NAMESPACE); this.version = version; this.affiliations = affiliations; @@ -82,7 +82,7 @@ public class MUCLightAffiliationsIQ extends IQ { * * @return the affiliations of the room */ - public HashMap getAffiliations() { + public Map getAffiliations() { return affiliations; } diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightBlockingIQ.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightBlockingIQ.java index f3334b651..fa6264ff0 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightBlockingIQ.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightBlockingIQ.java @@ -16,7 +16,6 @@ */ package org.jivesoftware.smackx.muclight.element; -import java.util.HashMap; import java.util.Iterator; import java.util.Map; @@ -38,8 +37,8 @@ public class MUCLightBlockingIQ extends IQ { public static final String ELEMENT = QUERY_ELEMENT; public static final String NAMESPACE = MultiUserChatLight.NAMESPACE + MultiUserChatLight.BLOCKING; - private final HashMap rooms; - private final HashMap users; + private final Map rooms; + private final Map users; /** * MUC Light blocking IQ constructor. @@ -47,7 +46,7 @@ public class MUCLightBlockingIQ extends IQ { * @param rooms TODO javadoc me please * @param users TODO javadoc me please */ - public MUCLightBlockingIQ(HashMap rooms, HashMap users) { + public MUCLightBlockingIQ(Map rooms, Map users) { super(ELEMENT, NAMESPACE); this.rooms = rooms; this.users = users; @@ -58,7 +57,7 @@ public class MUCLightBlockingIQ extends IQ { * * @return the rooms JIDs with booleans (true if allow, false if deny) */ - public HashMap getRooms() { + public Map getRooms() { return rooms; } @@ -67,7 +66,7 @@ public class MUCLightBlockingIQ extends IQ { * * @return the users JIDs with booleans (true if allow, false if deny) */ - public HashMap getUsers() { + public Map getUsers() { return users; } @@ -86,7 +85,7 @@ public class MUCLightBlockingIQ extends IQ { return xml; } - private static void parseBlocking(IQChildElementXmlStringBuilder xml, HashMap map, boolean isRoom) { + private static void parseBlocking(IQChildElementXmlStringBuilder xml, Map map, boolean isRoom) { Iterator> it = map.entrySet().iterator(); while (it.hasNext()) { Map.Entry pair = it.next(); diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightChangeAffiliationsIQ.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightChangeAffiliationsIQ.java index 494a7a4d7..e472c84e1 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightChangeAffiliationsIQ.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightChangeAffiliationsIQ.java @@ -16,7 +16,6 @@ */ package org.jivesoftware.smackx.muclight.element; -import java.util.HashMap; import java.util.Iterator; import java.util.Map; @@ -39,7 +38,7 @@ public class MUCLightChangeAffiliationsIQ extends IQ { public static final String ELEMENT = QUERY_ELEMENT; public static final String NAMESPACE = MultiUserChatLight.NAMESPACE + MultiUserChatLight.AFFILIATIONS; - private HashMap affiliations; + private Map affiliations; /** * MUCLight change affiliations IQ constructor. @@ -47,7 +46,8 @@ public class MUCLightChangeAffiliationsIQ extends IQ { * @param room TODO javadoc me please * @param affiliations TODO javadoc me please */ - public MUCLightChangeAffiliationsIQ(Jid room, HashMap affiliations) { + @SuppressWarnings("this-escape") + public MUCLightChangeAffiliationsIQ(Jid room, Map affiliations) { super(ELEMENT, NAMESPACE); this.setType(Type.set); this.setTo(room); @@ -59,7 +59,7 @@ public class MUCLightChangeAffiliationsIQ extends IQ { * * @return the affiliations */ - public HashMap getAffiliations() { + public Map getAffiliations() { return affiliations; } diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightCreateIQ.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightCreateIQ.java index d15d47db5..ea0db3b21 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightCreateIQ.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightCreateIQ.java @@ -18,6 +18,7 @@ package org.jivesoftware.smackx.muclight.element; import java.util.HashMap; import java.util.List; +import java.util.Map; import org.jivesoftware.smack.packet.IQ; @@ -42,7 +43,7 @@ public class MUCLightCreateIQ extends IQ { public static final String NAMESPACE = MultiUserChatLight.NAMESPACE + MultiUserChatLight.CREATE; private MUCLightRoomConfiguration configuration; - private final HashMap occupants; + private final Map occupants; /** * MUCLight create IQ constructor. @@ -53,7 +54,8 @@ public class MUCLightCreateIQ extends IQ { * @param customConfigs TODO javadoc me please * @param occupants TODO javadoc me please */ - public MUCLightCreateIQ(EntityJid room, String roomName, String subject, HashMap customConfigs, + @SuppressWarnings("this-escape") + public MUCLightCreateIQ(EntityJid room, String roomName, String subject, Map customConfigs, List occupants) { super(ELEMENT, NAMESPACE); this.configuration = new MUCLightRoomConfiguration(roomName, subject, customConfigs); @@ -92,7 +94,7 @@ public class MUCLightCreateIQ extends IQ { * * @return the room occupants */ - public HashMap getOccupants() { + public Map getOccupants() { return occupants; } diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightDestroyIQ.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightDestroyIQ.java index 8198007cf..6606ae035 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightDestroyIQ.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightDestroyIQ.java @@ -38,6 +38,7 @@ public class MUCLightDestroyIQ extends IQ { * * @param roomJid TODO javadoc me please */ + @SuppressWarnings("this-escape") public MUCLightDestroyIQ(Jid roomJid) { super(ELEMENT, NAMESPACE); this.setType(Type.set); diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightElements.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightElements.java index 33861b29e..27dcbd2e7 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightElements.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightElements.java @@ -16,7 +16,6 @@ */ package org.jivesoftware.smackx.muclight.element; -import java.util.HashMap; import java.util.Iterator; import java.util.Map; @@ -48,11 +47,11 @@ public abstract class MUCLightElements { public static final String NAMESPACE = MultiUserChatLight.NAMESPACE + MultiUserChatLight.AFFILIATIONS; public static final QName QNAME = new QName(NAMESPACE, ELEMENT); - private final HashMap affiliations; + private final Map affiliations; private final String prevVersion; private final String version; - public AffiliationsChangeExtension(HashMap affiliations, String prevVersion, + public AffiliationsChangeExtension(Map affiliations, String prevVersion, String version) { this.affiliations = affiliations; this.prevVersion = prevVersion; @@ -74,7 +73,7 @@ public abstract class MUCLightElements { * * @return the affiliations */ - public HashMap getAffiliations() { + public Map getAffiliations() { return affiliations; } @@ -135,7 +134,7 @@ public abstract class MUCLightElements { private final String version; private final String roomName; private final String subject; - private final HashMap customConfigs; + private final Map customConfigs; /** * Configurations change extension constructor. @@ -147,7 +146,7 @@ public abstract class MUCLightElements { * @param customConfigs TODO javadoc me please */ public ConfigurationsChangeExtension(String prevVersion, String version, String roomName, String subject, - HashMap customConfigs) { + Map customConfigs) { this.prevVersion = prevVersion; this.version = version; this.roomName = roomName; @@ -206,7 +205,7 @@ public abstract class MUCLightElements { * * @return the room custom configurations */ - public HashMap getCustomConfigs() { + public Map getCustomConfigs() { return customConfigs; } @@ -287,14 +286,14 @@ public abstract class MUCLightElements { */ public static class OccupantsElement implements Element { - private HashMap occupants; + private Map occupants; /** * Occupants element constructor. * * @param occupants TODO javadoc me please */ - public OccupantsElement(HashMap occupants) { + public OccupantsElement(Map occupants) { this.occupants = occupants; } diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightGetAffiliationsIQ.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightGetAffiliationsIQ.java index 67cd26481..70249914c 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightGetAffiliationsIQ.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightGetAffiliationsIQ.java @@ -41,6 +41,7 @@ public class MUCLightGetAffiliationsIQ extends IQ { * @param roomJid TODO javadoc me please * @param version TODO javadoc me please */ + @SuppressWarnings("this-escape") public MUCLightGetAffiliationsIQ(Jid roomJid, String version) { super(ELEMENT, NAMESPACE); this.version = version; diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightGetConfigsIQ.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightGetConfigsIQ.java index 15ce3e1f9..aff4283ee 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightGetConfigsIQ.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightGetConfigsIQ.java @@ -41,6 +41,7 @@ public class MUCLightGetConfigsIQ extends IQ { * @param roomJid TODO javadoc me please * @param version TODO javadoc me please */ + @SuppressWarnings("this-escape") public MUCLightGetConfigsIQ(Jid roomJid, String version) { super(ELEMENT, NAMESPACE); this.version = version; diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightGetInfoIQ.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightGetInfoIQ.java index 112ec1935..6e3d1daa0 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightGetInfoIQ.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightGetInfoIQ.java @@ -41,6 +41,7 @@ public class MUCLightGetInfoIQ extends IQ { * @param roomJid TODO javadoc me please * @param version TODO javadoc me please */ + @SuppressWarnings("this-escape") public MUCLightGetInfoIQ(Jid roomJid, String version) { super(ELEMENT, NAMESPACE); this.version = version; diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightInfoIQ.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightInfoIQ.java index 42a2b9308..ef33d4b77 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightInfoIQ.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightInfoIQ.java @@ -16,7 +16,7 @@ */ package org.jivesoftware.smackx.muclight.element; -import java.util.HashMap; +import java.util.Map; import org.jivesoftware.smack.packet.IQ; @@ -41,7 +41,7 @@ public class MUCLightInfoIQ extends IQ { private final String version; private final MUCLightRoomConfiguration configuration; - private final HashMap occupants; + private final Map occupants; /** * MUCLight info response IQ constructor. @@ -51,7 +51,7 @@ public class MUCLightInfoIQ extends IQ { * @param occupants TODO javadoc me please */ public MUCLightInfoIQ(String version, MUCLightRoomConfiguration configuration, - HashMap occupants) { + Map occupants) { super(ELEMENT, NAMESPACE); this.version = version; this.configuration = configuration; @@ -90,7 +90,7 @@ public class MUCLightInfoIQ extends IQ { * * @return the occupants of the room */ - public HashMap getOccupants() { + public Map getOccupants() { return occupants; } diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightSetConfigsIQ.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightSetConfigsIQ.java index 0751b582f..3e332194a 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightSetConfigsIQ.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/element/MUCLightSetConfigsIQ.java @@ -16,7 +16,6 @@ */ package org.jivesoftware.smackx.muclight.element; -import java.util.HashMap; import java.util.Iterator; import java.util.Map; @@ -39,7 +38,7 @@ public class MUCLightSetConfigsIQ extends IQ { private String roomName; private String subject; - private HashMap customConfigs; + private Map customConfigs; /** * MUC Light set configuration IQ constructor. @@ -49,7 +48,8 @@ public class MUCLightSetConfigsIQ extends IQ { * @param subject TODO javadoc me please * @param customConfigs TODO javadoc me please */ - public MUCLightSetConfigsIQ(Jid roomJid, String roomName, String subject, HashMap customConfigs) { + @SuppressWarnings("this-escape") + public MUCLightSetConfigsIQ(Jid roomJid, String roomName, String subject, Map customConfigs) { super(ELEMENT, NAMESPACE); this.roomName = roomName; this.subject = subject; @@ -65,7 +65,7 @@ public class MUCLightSetConfigsIQ extends IQ { * @param roomName TODO javadoc me please * @param customConfigs TODO javadoc me please */ - public MUCLightSetConfigsIQ(Jid roomJid, String roomName, HashMap customConfigs) { + public MUCLightSetConfigsIQ(Jid roomJid, String roomName, Map customConfigs) { this(roomJid, roomName, null, customConfigs); } diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/provider/MUCLightBlockingIQProvider.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/provider/MUCLightBlockingIQProvider.java index b89dba5c8..1412b2900 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/provider/MUCLightBlockingIQProvider.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/provider/MUCLightBlockingIQProvider.java @@ -18,6 +18,7 @@ package org.jivesoftware.smackx.muclight.provider; import java.io.IOException; import java.util.HashMap; +import java.util.Map; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.IqData; @@ -42,8 +43,8 @@ public class MUCLightBlockingIQProvider extends IqProvider { @Override public MUCLightBlockingIQ parse(XmlPullParser parser, int initialDepth, IqData iqData, XmlEnvironment xmlEnvironment) throws XmlPullParserException, IOException { - HashMap rooms = null; - HashMap users = null; + Map rooms = null; + Map users = null; outerloop: while (true) { XmlPullParser.Event eventType = parser.next(); @@ -70,7 +71,7 @@ public class MUCLightBlockingIQProvider extends IqProvider { return mucLightBlockingIQ; } - private static HashMap parseBlocking(XmlPullParser parser, HashMap map) + private static Map parseBlocking(XmlPullParser parser, Map map) throws XmppStringprepException, XmlPullParserException, IOException { if (map == null) { map = new HashMap<>(); diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/provider/MUCLightInfoIQProvider.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/provider/MUCLightInfoIQProvider.java index 54d0a4218..50d30bca4 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/provider/MUCLightInfoIQProvider.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/provider/MUCLightInfoIQProvider.java @@ -18,6 +18,7 @@ package org.jivesoftware.smackx.muclight.provider; import java.io.IOException; import java.util.HashMap; +import java.util.Map; import org.jivesoftware.smack.packet.IqData; import org.jivesoftware.smack.packet.XmlEnvironment; @@ -45,8 +46,8 @@ public class MUCLightInfoIQProvider extends IqProvider { String version = null; String roomName = null; String subject = null; - HashMap customConfigs = null; - HashMap occupants = new HashMap<>(); + Map customConfigs = null; + Map occupants = new HashMap<>(); outerloop: while (true) { XmlPullParser.Event eventType = parser.next(); @@ -97,8 +98,8 @@ public class MUCLightInfoIQProvider extends IqProvider { return new MUCLightInfoIQ(version, new MUCLightRoomConfiguration(roomName, subject, customConfigs), occupants); } - private static HashMap iterateOccupants(XmlPullParser parser) throws XmlPullParserException, IOException { - HashMap occupants = new HashMap<>(); + private static Map iterateOccupants(XmlPullParser parser) throws XmlPullParserException, IOException { + Map occupants = new HashMap<>(); int depth = parser.getDepth(); outerloop: while (true) { diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/push_notifications/PushNotificationsManager.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/push_notifications/PushNotificationsManager.java index ff62d96eb..8615b4759 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/push_notifications/PushNotificationsManager.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/push_notifications/PushNotificationsManager.java @@ -16,7 +16,6 @@ */ package org.jivesoftware.smackx.push_notifications; -import java.util.HashMap; import java.util.Map; import java.util.WeakHashMap; @@ -122,7 +121,7 @@ public final class PushNotificationsManager extends Manager { * @throws NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. */ - public boolean enable(Jid pushJid, String node, HashMap publishOptions) + public boolean enable(Jid pushJid, String node, Map publishOptions) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { EnablePushNotificationsIQ enablePushNotificationsIQ = new EnablePushNotificationsIQ(pushJid, node, publishOptions); diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/push_notifications/element/DisablePushNotificationsIQ.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/push_notifications/element/DisablePushNotificationsIQ.java index bedf8be02..08342811b 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/push_notifications/element/DisablePushNotificationsIQ.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/push_notifications/element/DisablePushNotificationsIQ.java @@ -43,6 +43,7 @@ public class DisablePushNotificationsIQ extends IQ { private final Jid jid; private final String node; + @SuppressWarnings("this-escape") public DisablePushNotificationsIQ(Jid jid, String node) { super(ELEMENT, NAMESPACE); this.jid = jid; diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/push_notifications/element/EnablePushNotificationsIQ.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/push_notifications/element/EnablePushNotificationsIQ.java index 428b41b67..c6f8199b4 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/push_notifications/element/EnablePushNotificationsIQ.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/push_notifications/element/EnablePushNotificationsIQ.java @@ -16,7 +16,6 @@ */ package org.jivesoftware.smackx.push_notifications.element; -import java.util.HashMap; import java.util.Iterator; import java.util.Map; @@ -51,9 +50,10 @@ public class EnablePushNotificationsIQ extends IQ { private final Jid jid; private final String node; - private final HashMap publishOptions; + private final Map publishOptions; - public EnablePushNotificationsIQ(Jid jid, String node, HashMap publishOptions) { + @SuppressWarnings("this-escape") + public EnablePushNotificationsIQ(Jid jid, String node, Map publishOptions) { super(ELEMENT, NAMESPACE); this.jid = jid; this.node = node; @@ -88,7 +88,7 @@ public class EnablePushNotificationsIQ extends IQ { * * @return the publish options */ - public HashMap getPublishOptions() { + public Map getPublishOptions() { return publishOptions; } diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/reference/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/reference/package-info.java index 821577179..fc8f020be 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/reference/package-info.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/reference/package-info.java @@ -29,7 +29,7 @@ * *

        * 
      - * Message message = new Message("Alice is a realy nice person.");
      + * Message message = new Message("Alice is a really nice person.");
        * BareJid alice = JidCreate.bareFrom("alice@capulet.lit");
        * ReferenceManager.addMention(message, 0, 5, alice);
        * 
      diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/sid/element/OriginIdElement.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/sid/element/OriginIdElement.java
      index a50fd99e2..f0766de8b 100644
      --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/sid/element/OriginIdElement.java
      +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/sid/element/OriginIdElement.java
      @@ -38,28 +38,6 @@ public class OriginIdElement extends StableAndUniqueIdElement {
               super(id);
           }
       
      -    /**
      -     * Add an origin-id element to a message and set the stanzas id to the same id as in the origin-id element.
      -     *
      -     * @param message message.
      -     * @return the added origin-id element.
      -     * @deprecated use {@link #addTo(MessageBuilder)} instead.
      -     */
      -    @Deprecated
      -    // TODO: Remove in Smack 4.5.
      -    public static OriginIdElement addOriginId(Message message) {
      -        OriginIdElement originId = message.getExtension(OriginIdElement.class);
      -        if (originId != null) {
      -            return originId;
      -        }
      -
      -        originId = new OriginIdElement();
      -        message.addExtension(originId);
      -        // TODO: Find solution to have both the originIds stanzaId and a nice to look at incremental stanzaID.
      -        // message.setStanzaId(originId.getId());
      -        return originId;
      -    }
      -
           /**
            * Add an origin-id element to a message and set the stanzas id to the same id as in the origin-id element.
            *
      diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/spoiler/element/SpoilerElement.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/spoiler/element/SpoilerElement.java
      index 6d5c02fd6..b18a5af5c 100644
      --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/spoiler/element/SpoilerElement.java
      +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/spoiler/element/SpoilerElement.java
      @@ -114,6 +114,7 @@ public class SpoilerElement implements ExtensionElement {
            * @param message message
            * @return map of spoilers
            */
      +    @SuppressWarnings("MixedMutabilityReturnType")
           public static Map getSpoilers(Message message) {
               if (!containsSpoiler(message)) {
                   return Collections.emptyMap();
      diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/stanza_content_encryption/element/TimestampAffixElement.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/stanza_content_encryption/element/TimestampAffixElement.java
      index 0c48f7b7a..eee726085 100644
      --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/stanza_content_encryption/element/TimestampAffixElement.java
      +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/stanza_content_encryption/element/TimestampAffixElement.java
      @@ -56,6 +56,7 @@ public class TimestampAffixElement implements NamedElement, AffixElement {
               return EqualsUtil.equals(this, obj, (e, o) -> e.append(getTimestamp(), o.getTimestamp()));
           }
       
      +    @SuppressWarnings("JavaUtilDate")
           @Override
           public int hashCode() {
               return timestamp.hashCode();
      diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/thumbnails/element/ThumbnailElement.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/thumbnails/element/ThumbnailElement.java
      new file mode 100644
      index 000000000..3bcd4b045
      --- /dev/null
      +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/thumbnails/element/ThumbnailElement.java
      @@ -0,0 +1,92 @@
      +/**
      + *
      + * Copyright 2023 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.thumbnails.element;
      +
      +import org.jivesoftware.smack.packet.ExtensionElement;
      +import org.jivesoftware.smack.packet.XmlEnvironment;
      +import org.jivesoftware.smack.util.Objects;
      +import org.jivesoftware.smack.util.XmlStringBuilder;
      +
      +public class ThumbnailElement implements ExtensionElement {
      +
      +    public static final String ELEMENT = "thumbnail";
      +    public static final String NAMESPACE = "urn:xmpp:thumbs:1";
      +    public static final String ELEM_URI = "uri";
      +    public static final String ELEM_MEDIA_TYPE = "media-type";
      +    public static final String ELEM_WIDTH = "width";
      +    public static final String ELEM_HEIGHT = "height";
      +
      +    private final String uri;
      +    private final String mediaType;
      +    private final Integer width;
      +    private final Integer height;
      +
      +    public ThumbnailElement(String uri) {
      +        this(uri, null, null, null);
      +    }
      +
      +    public ThumbnailElement(String uri, String mediaType, Integer width, Integer height) {
      +        this.uri = Objects.requireNonNull(uri);
      +        this.mediaType = mediaType;
      +
      +        if (width != null && width < 0) {
      +            throw new IllegalArgumentException("Width cannot be negative.");
      +        }
      +        this.width = width;
      +
      +        if (height != null && height < 0) {
      +            throw new IllegalArgumentException("Height cannot be negative.");
      +        }
      +        this.height = height;
      +    }
      +
      +    public String getUri() {
      +        return uri;
      +    }
      +
      +    public String getMediaType() {
      +        return mediaType;
      +    }
      +
      +    public Integer getWidth() {
      +        return width;
      +    }
      +
      +    public Integer getHeight() {
      +        return height;
      +    }
      +
      +    @Override
      +    public CharSequence toXML(XmlEnvironment xmlEnvironment) {
      +        XmlStringBuilder sb = new XmlStringBuilder(this, xmlEnvironment);
      +        return sb.attribute(ELEM_URI, uri)
      +                .optAttribute(ELEM_MEDIA_TYPE, mediaType)
      +                .optAttribute(ELEM_WIDTH, width)
      +                .optAttribute(ELEM_HEIGHT, height)
      +                .closeEmptyElement();
      +    }
      +
      +    @Override
      +    public String getElementName() {
      +        return ELEMENT;
      +    }
      +
      +    @Override
      +    public String getNamespace() {
      +        return NAMESPACE;
      +    }
      +}
      diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/thumbnails/element/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/thumbnails/element/package-info.java
      new file mode 100644
      index 000000000..a13671247
      --- /dev/null
      +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/thumbnails/element/package-info.java
      @@ -0,0 +1,20 @@
      +/**
      + *
      + * Copyright 2023 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.
      + */
      +/**
      + * Smacks implementation of XEP-0264: Jingle Content Thumbnails.
      + */
      +package org.jivesoftware.smackx.thumbnails.element;
      diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/thumbnails/provider/ThumbnailElementProvider.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/thumbnails/provider/ThumbnailElementProvider.java
      new file mode 100644
      index 000000000..6fc174002
      --- /dev/null
      +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/thumbnails/provider/ThumbnailElementProvider.java
      @@ -0,0 +1,46 @@
      +/**
      + *
      + * Copyright 2023 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.thumbnails.provider;
      +
      +import java.io.IOException;
      +import java.text.ParseException;
      +
      +import org.jivesoftware.smack.packet.XmlEnvironment;
      +import org.jivesoftware.smack.parsing.SmackParsingException;
      +import org.jivesoftware.smack.provider.ExtensionElementProvider;
      +import org.jivesoftware.smack.util.ParserUtils;
      +import org.jivesoftware.smack.xml.XmlPullParser;
      +import org.jivesoftware.smack.xml.XmlPullParserException;
      +import org.jivesoftware.smackx.thumbnails.element.ThumbnailElement;
      +
      +public class ThumbnailElementProvider extends ExtensionElementProvider {
      +    @Override
      +    public ThumbnailElement parse(XmlPullParser parser, int initialDepth, XmlEnvironment xmlEnvironment)
      +            throws XmlPullParserException, IOException, SmackParsingException, ParseException {
      +        String uri = parser.getAttributeValue(ThumbnailElement.ELEM_URI);
      +        String mediaType = parser.getAttributeValue(ThumbnailElement.ELEM_MEDIA_TYPE);
      +        Integer width = ParserUtils.getIntegerAttribute(parser, ThumbnailElement.ELEM_WIDTH);
      +        Integer height = ParserUtils.getIntegerAttribute(parser, ThumbnailElement.ELEM_HEIGHT);
      +
      +        return new ThumbnailElement(
      +                uri,
      +                mediaType,
      +                width,
      +                height
      +        );
      +    }
      +}
      diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/thumbnails/provider/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/thumbnails/provider/package-info.java
      new file mode 100644
      index 000000000..20816c2ce
      --- /dev/null
      +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/thumbnails/provider/package-info.java
      @@ -0,0 +1,20 @@
      +/**
      + *
      + * Copyright 2023 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.
      + */
      +/**
      + * Smacks implementation of XEP-0264: Jingle Content Thumbnails.
      + */
      +package org.jivesoftware.smackx.thumbnails.provider;
      diff --git a/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers b/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers
      index eb0c1a45f..822bc5837 100644
      --- a/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers
      +++ b/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers
      @@ -2,6 +2,13 @@
       
       
       
      +    
      +    
      +        thumbnail
      +        urn:xmpp:thumbs:1
      +        org.jivesoftware.smackx.thumbnails.provider.ThumbnailElementProvider
      +    
      +
           
           
               sent
      @@ -345,6 +352,13 @@
             org.jivesoftware.smackx.fallback_indication.provider.FallbackIndicationElementProvider
           
       
      +    
      +    
      +      file
      +      urn:xmpp:file:metadata:0
      +      org.jivesoftware.smackx.file_metadata.provider.FileMetadataElementProvider
      +    
      +
           
           
               query
      diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/eme/ExplicitMessageEncryptionElementTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/eme/ExplicitMessageEncryptionElementTest.java
      index dc14db131..b9364b01f 100644
      --- a/smack-experimental/src/test/java/org/jivesoftware/smackx/eme/ExplicitMessageEncryptionElementTest.java
      +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/eme/ExplicitMessageEncryptionElementTest.java
      @@ -39,7 +39,7 @@ public class ExplicitMessageEncryptionElementTest extends SmackTestSuite {
           public void addToMessageTest() {
               Message message = StanzaBuilder.buildMessage().build();
       
      -        // Check inital state (no elements)
      +        // Check initial state (no elements)
               assertNull(ExplicitMessageEncryptionElement.from(message));
               assertFalse(ExplicitMessageEncryptionElement.hasProtocol(message,
                       ExplicitMessageEncryptionElement.ExplicitMessageEncryptionProtocol.omemoVAxolotl));
      @@ -75,7 +75,7 @@ public class ExplicitMessageEncryptionElementTest extends SmackTestSuite {
               assertTrue(ExplicitMessageEncryptionElement.hasProtocol(message,
                       ExplicitMessageEncryptionElement.ExplicitMessageEncryptionProtocol.omemoVAxolotl));
       
      -        // Check, if adding additional OMEMO wont add another element
      +        // Check, if adding additional OMEMO won't add another element
               ExplicitMessageEncryptionElement.set(messageBuilder,
                       ExplicitMessageEncryptionElement.ExplicitMessageEncryptionProtocol.omemoVAxolotl);
       
      diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/file_metadata/FileMetadataElementTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/file_metadata/FileMetadataElementTest.java
      new file mode 100644
      index 000000000..229581308
      --- /dev/null
      +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/file_metadata/FileMetadataElementTest.java
      @@ -0,0 +1,190 @@
      +/**
      + *
      + * 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.file_metadata;
      +
      +import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar;
      +import static org.junit.jupiter.api.Assertions.assertEquals;
      +import static org.junit.jupiter.api.Assertions.assertNull;
      +import static org.junit.jupiter.api.Assertions.assertThrows;
      +
      +import java.text.ParseException;
      +import java.util.Date;
      +
      +import org.jivesoftware.smack.test.util.SmackTestSuite;
      +import org.jivesoftware.smack.test.util.SmackTestUtil;
      +import org.jivesoftware.smackx.file_metadata.element.FileMetadataElement;
      +import org.jivesoftware.smackx.file_metadata.provider.FileMetadataElementProvider;
      +import org.jivesoftware.smackx.hashes.HashManager;
      +import org.jivesoftware.smackx.hashes.element.HashElement;
      +
      +import org.junit.jupiter.api.Test;
      +import org.junit.jupiter.params.ParameterizedTest;
      +import org.junit.jupiter.params.provider.EnumSource;
      +import org.jxmpp.util.XmppDateTime;
      +
      +public class FileMetadataElementTest extends SmackTestSuite {
      +
      +    private static final Date date;
      +    static {
      +        try {
      +            date = XmppDateTime.parseDate("2015-07-26T21:46:00+01:00");
      +        } catch (ParseException e) {
      +            throw new IllegalStateException(e);
      +        }
      +    }
      +
      +    private static final FileMetadataElement metadataElement = FileMetadataElement.builder()
      +                    .setModificationDate(date)
      +                    .setWidth(1920)
      +                    .setHeight(1080)
      +                    .addDescription("Picture of 24th XSF Summit")
      +                    .addDescription("Foto vom 24. XSF Summit", "de")
      +                    .addHash(new HashElement(HashManager.ALGORITHM.SHA_256, "2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU="))
      +                    .setLength(63000)
      +                    .setMediaType("text/plain")
      +                    .setName("text.txt")
      +                    .setSize(6144)
      +                    .build();
      +
      +    private static final String expectedXml = "" +
      +            "2015-07-26T20:46:00.000+00:00" +
      +            "1920" +
      +            "1080" +
      +            "Picture of 24th XSF Summit" +
      +            "Foto vom 24. XSF Summit" +
      +            "2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU=" +
      +            "63000" +
      +            "text/plain" +
      +            "text.txt" +
      +            "6144" +
      +            "";
      +
      +    private static final String expectedLegacyXml = "" +
      +            "2015-07-26T20:46:00.000+00:00" +
      +            "1920x1080" +
      +            "Picture of 24th XSF Summit" +
      +            "Foto vom 24. XSF Summit" +
      +            "2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU=" +
      +            "63000" +
      +            "text/plain" +
      +            "text.txt" +
      +            "6144" +
      +            "";
      +
      +    @Test
      +    public void testSerialization() {
      +        assertXmlSimilar(expectedXml, metadataElement.toXML().toString());
      +    }
      +
      +    @ParameterizedTest
      +    @EnumSource(SmackTestUtil.XmlPullParserKind.class)
      +    public void testParsing(SmackTestUtil.XmlPullParserKind parserKind) throws Exception {
      +        FileMetadataElement parsed = SmackTestUtil.parse(expectedXml, FileMetadataElementProvider.class, parserKind);
      +
      +        assertEquals(metadataElement, parsed);
      +    }
      +
      +    @ParameterizedTest
      +    @EnumSource(SmackTestUtil.XmlPullParserKind.class)
      +    public void testLegacyParsing(SmackTestUtil.XmlPullParserKind parserKind) throws Exception {
      +        FileMetadataElement parsed = SmackTestUtil.parse(expectedLegacyXml, FileMetadataElementProvider.class, parserKind);
      +
      +        assertEquals(metadataElement, parsed);
      +    }
      +
      +    @ParameterizedTest
      +    @EnumSource(SmackTestUtil.XmlPullParserKind.class)
      +    public void testParseUnknownExtension(SmackTestUtil.XmlPullParserKind parserKind) throws Exception {
      +        final String xml = "" +
      +                        "2015-07-26T20:46:00.000+00:00" +
      +                        "1920" +
      +                        "1080" +
      +                        "foo" +
      +                        "Picture of 24th XSF Summit" +
      +                        "Foto vom 24. XSF Summit" +
      +                        "2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU=" +
      +                        "63000" +
      +                        "text/plain" +
      +                        "text.txt" +
      +                        "6144" +
      +                        "";
      +
      +        FileMetadataElement parsed = SmackTestUtil.parse(xml, FileMetadataElementProvider.class, parserKind);
      +
      +        assertEquals(metadataElement, parsed);
      +    }
      +
      +    @Test
      +    public void nameIsEscaped() {
      +        FileMetadataElement e = FileMetadataElement.builder().setName("/etc/passwd").build();
      +        assertEquals("%2Fetc%2Fpasswd", e.getName());
      +    }
      +
      +    @Test
      +    public void rejectNegativeSize() {
      +        assertThrows(IllegalArgumentException.class, () -> FileMetadataElement.builder().setSize(-1));
      +    }
      +
      +    @Test
      +    public void rejectNegativeLength() {
      +        assertThrows(IllegalArgumentException.class, () -> FileMetadataElement.builder().setLength(-1));
      +    }
      +
      +    @Test
      +    public void rejectNegativeWidth() {
      +        assertThrows(IllegalArgumentException.class, () -> FileMetadataElement.builder().setWidth(-1));
      +    }
      +
      +    @Test
      +    public void rejectNegativeHeight() {
      +        assertThrows(IllegalArgumentException.class, () -> FileMetadataElement.builder().setHeight(-1));
      +    }
      +
      +    @Test
      +    public void rejectEmptyDescription() {
      +        assertThrows(IllegalArgumentException.class, () -> FileMetadataElement.builder().addDescription(""));
      +        assertThrows(IllegalArgumentException.class, () -> FileMetadataElement.builder().addDescription(null));
      +    }
      +
      +    @Test
      +    public void rejectEmptyNameElement() {
      +        assertThrows(IllegalArgumentException.class, () -> FileMetadataElement.builder().setName(""));
      +        assertThrows(IllegalArgumentException.class, () -> FileMetadataElement.builder().setName(null));
      +    }
      +
      +    @Test
      +    public void rejectEmptyMediaTypeElement() {
      +        assertThrows(IllegalArgumentException.class, () -> FileMetadataElement.builder().setMediaType(""));
      +        assertThrows(IllegalArgumentException.class, () -> FileMetadataElement.builder().setMediaType(null));
      +    }
      +
      +    @Test
      +    public void getDescTest() {
      +        FileMetadataElement metadataElement = FileMetadataElement.builder()
      +                .addDescription("Foo", "br")
      +                .addDescription("Baz")
      +                .addDescription("Bag", "en")
      +                .build();
      +
      +        assertEquals("Foo", metadataElement.getDescription("br"));
      +        assertEquals("Baz", metadataElement.getDescription(null));
      +        assertEquals("Baz", metadataElement.getDescription());
      +        assertEquals("Bag", metadataElement.getDescription("en"));
      +        assertNull(metadataElement.getDescription("null"));
      +        assertEquals(3, metadataElement.getDescriptions().size());
      +    }
      +}
      diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/FiltersTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/FiltersTest.java
      index b5c974dce..df14ea279 100644
      --- a/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/FiltersTest.java
      +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/FiltersTest.java
      @@ -1,6 +1,6 @@
       /**
        *
      - * Copyright 2016 Fernando Ramirez, 2018-2020 Florian Schmaus
      + * Copyright 2016 Fernando Ramirez, 2018-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.
      @@ -46,6 +46,7 @@ public class FiltersTest extends MamTest {
               return xml;
           }
       
      +    @SuppressWarnings("JavaUtilDate")
           @Test
           public void checkStartDateFilter() throws Exception {
               Date date = new Date();
      @@ -61,6 +62,7 @@ public class FiltersTest extends MamTest {
               assertEquals(getMamXMemberWith(fields, values), dataForm.toXML().toString());
           }
       
      +    @SuppressWarnings("JavaUtilDate")
           @Test
           public void checkEndDateFilter() throws Exception {
               Date date = new Date();
      diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/muclight/MUCLightAffiliationsChangeExtensionTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/muclight/MUCLightAffiliationsChangeExtensionTest.java
      index a07577748..ab9e53750 100644
      --- a/smack-experimental/src/test/java/org/jivesoftware/smackx/muclight/MUCLightAffiliationsChangeExtensionTest.java
      +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/muclight/MUCLightAffiliationsChangeExtensionTest.java
      @@ -18,7 +18,7 @@ package org.jivesoftware.smackx.muclight;
       
       import static org.junit.jupiter.api.Assertions.assertEquals;
       
      -import java.util.HashMap;
      +import java.util.Map;
       
       import org.jivesoftware.smack.packet.Message;
       import org.jivesoftware.smack.util.PacketParserUtils;
      @@ -55,7 +55,7 @@ public class MUCLightAffiliationsChangeExtensionTest {
               AffiliationsChangeExtension affiliationsChangeExtension = AffiliationsChangeExtension
                       .from(changeAffiliationsMessage);
       
      -        HashMap affiliations = affiliationsChangeExtension.getAffiliations();
      +        Map affiliations = affiliationsChangeExtension.getAffiliations();
               assertEquals(affiliations.size(), 3);
               assertEquals(affiliations.get(JidCreate.from("sarasa2@shakespeare.lit")), MUCLightAffiliation.owner);
               assertEquals(affiliations.get(JidCreate.from("sarasa1@shakespeare.lit")), MUCLightAffiliation.member);
      @@ -68,7 +68,7 @@ public class MUCLightAffiliationsChangeExtensionTest {
               AffiliationsChangeExtension affiliationsChangeExtension = AffiliationsChangeExtension
                       .from(changeAffiliationsMessage);
       
      -        HashMap affiliations = affiliationsChangeExtension.getAffiliations();
      +        Map affiliations = affiliationsChangeExtension.getAffiliations();
               assertEquals(affiliations.size(), 2);
               assertEquals(affiliations.get(JidCreate.from("sarasa1@shakespeare.lit")), MUCLightAffiliation.member);
               assertEquals(affiliations.get(JidCreate.from("sarasa3@shakespeare.lit")), MUCLightAffiliation.none);
      @@ -83,7 +83,7 @@ public class MUCLightAffiliationsChangeExtensionTest {
               AffiliationsChangeExtension affiliationsChangeExtension = AffiliationsChangeExtension
                       .from(changeAffiliationsMessage);
       
      -        HashMap affiliations = affiliationsChangeExtension.getAffiliations();
      +        Map affiliations = affiliationsChangeExtension.getAffiliations();
               assertEquals(affiliations.size(), 2);
               assertEquals(affiliations.get(JidCreate.from("sarasa2@shakespeare.lit")), MUCLightAffiliation.owner);
               assertEquals(affiliations.get(JidCreate.from("sarasa1@shakespeare.lit")), MUCLightAffiliation.member);
      diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/muclight/MUCLightChangeAffiliationsIQTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/muclight/MUCLightChangeAffiliationsIQTest.java
      index efbb22da7..ca0e1e4cb 100644
      --- a/smack-experimental/src/test/java/org/jivesoftware/smackx/muclight/MUCLightChangeAffiliationsIQTest.java
      +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/muclight/MUCLightChangeAffiliationsIQTest.java
      @@ -19,6 +19,7 @@ package org.jivesoftware.smackx.muclight;
       import static org.junit.jupiter.api.Assertions.assertEquals;
       
       import java.util.HashMap;
      +import java.util.Map;
       
       import org.jivesoftware.smack.packet.IQ;
       
      @@ -32,7 +33,7 @@ public class MUCLightChangeAffiliationsIQTest {
       
           @Test
           public void checkChangeAffiliationsMUCLightStanza() throws Exception {
      -        HashMap affiliations = new HashMap<>();
      +        Map affiliations = new HashMap<>();
               affiliations.put(JidCreate.from("sarasa2@shakespeare.lit"), MUCLightAffiliation.owner);
               affiliations.put(JidCreate.from("sarasa1@shakespeare.lit"), MUCLightAffiliation.member);
               affiliations.put(JidCreate.from("sarasa3@shakespeare.lit"), MUCLightAffiliation.none);
      @@ -44,7 +45,7 @@ public class MUCLightChangeAffiliationsIQTest {
               assertEquals(mucLightChangeAffiliationsIQ.getTo(), "coven@muclight.shakespeare.lit");
               assertEquals(mucLightChangeAffiliationsIQ.getType(), IQ.Type.set);
       
      -        HashMap iqAffiliations = mucLightChangeAffiliationsIQ.getAffiliations();
      +        Map iqAffiliations = mucLightChangeAffiliationsIQ.getAffiliations();
               assertEquals(iqAffiliations.get(JidCreate.from("sarasa1@shakespeare.lit")), MUCLightAffiliation.member);
               assertEquals(iqAffiliations.get(JidCreate.from("sarasa2@shakespeare.lit")), MUCLightAffiliation.owner);
               assertEquals(iqAffiliations.get(JidCreate.from("sarasa3@shakespeare.lit")), MUCLightAffiliation.none);
      diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/muclight/MUCLightCreateIQTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/muclight/MUCLightCreateIQTest.java
      index 216a2a494..4a92da24e 100644
      --- a/smack-experimental/src/test/java/org/jivesoftware/smackx/muclight/MUCLightCreateIQTest.java
      +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/muclight/MUCLightCreateIQTest.java
      @@ -19,8 +19,8 @@ package org.jivesoftware.smackx.muclight;
       import static org.junit.jupiter.api.Assertions.assertEquals;
       
       import java.util.ArrayList;
      -import java.util.HashMap;
       import java.util.List;
      +import java.util.Map;
       
       import org.jivesoftware.smackx.muclight.element.MUCLightCreateIQ;
       
      @@ -43,7 +43,7 @@ public class MUCLightCreateIQTest {
       
               assertEquals(mucLightCreateIQ.getConfiguration().getRoomName(), "test");
       
      -        HashMap iqOccupants = mucLightCreateIQ.getOccupants();
      +        Map iqOccupants = mucLightCreateIQ.getOccupants();
               assertEquals(iqOccupants.get(JidCreate.from("charlie@test.com")), MUCLightAffiliation.member);
               assertEquals(iqOccupants.get(JidCreate.from("pep@test.com")), MUCLightAffiliation.member);
           }
      diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/muclight/MUCLightGetAffiliationsTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/muclight/MUCLightGetAffiliationsTest.java
      index 782171ae1..5504468ef 100644
      --- a/smack-experimental/src/test/java/org/jivesoftware/smackx/muclight/MUCLightGetAffiliationsTest.java
      +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/muclight/MUCLightGetAffiliationsTest.java
      @@ -18,7 +18,7 @@ package org.jivesoftware.smackx.muclight;
       
       import static org.junit.jupiter.api.Assertions.assertEquals;
       
      -import java.util.HashMap;
      +import java.util.Map;
       
       import org.jivesoftware.smack.packet.IQ;
       import org.jivesoftware.smack.packet.StreamOpen;
      @@ -57,7 +57,7 @@ public class MUCLightGetAffiliationsTest {
       
               assertEquals("123456", mucLightAffiliationsIQ.getVersion());
       
      -        HashMap affiliations = mucLightAffiliationsIQ.getAffiliations();
      +        Map affiliations = mucLightAffiliationsIQ.getAffiliations();
               assertEquals(3, affiliations.size());
               assertEquals(MUCLightAffiliation.owner, affiliations.get(JidCreate.from("user1@shakespeare.lit")));
               assertEquals(MUCLightAffiliation.member, affiliations.get(JidCreate.from("user2@shakespeare.lit")));
      diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/muclight/MUCLightGetConfigsTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/muclight/MUCLightGetConfigsTest.java
      index 8c295ab0e..02826d8d1 100644
      --- a/smack-experimental/src/test/java/org/jivesoftware/smackx/muclight/MUCLightGetConfigsTest.java
      +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/muclight/MUCLightGetConfigsTest.java
      @@ -19,7 +19,7 @@ package org.jivesoftware.smackx.muclight;
       import static org.junit.jupiter.api.Assertions.assertEquals;
       import static org.junit.jupiter.api.Assertions.assertNull;
       
      -import java.util.HashMap;
      +import java.util.Map;
       
       import org.jivesoftware.smack.packet.IQ;
       import org.jivesoftware.smack.packet.StreamOpen;
      @@ -74,7 +74,7 @@ public class MUCLightGetConfigsTest {
               assertEquals("A Dark Cave", mucLightConfigurationIQ.getConfiguration().getRoomName());
               assertNull(mucLightConfigurationIQ.getConfiguration().getSubject());
       
      -        HashMap customConfigs = mucLightConfigurationIQ.getConfiguration().getCustomConfigs();
      +        Map customConfigs = mucLightConfigurationIQ.getConfiguration().getCustomConfigs();
               assertEquals("blue", customConfigs.get("color"));
               assertEquals("20", customConfigs.get("size"));
           }
      diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/thumbnails/element/ThumbnailElementTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/thumbnails/element/ThumbnailElementTest.java
      new file mode 100644
      index 000000000..87d03bbb6
      --- /dev/null
      +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/thumbnails/element/ThumbnailElementTest.java
      @@ -0,0 +1,56 @@
      +/**
      + *
      + * Copyright 2023 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.thumbnails.element;
      +
      +import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar;
      +import static org.junit.jupiter.api.Assertions.assertThrows;
      +
      +import org.junit.jupiter.api.Test;
      +
      +public class ThumbnailElementTest {
      +
      +    @Test
      +    public void uriIsRequired() {
      +        assertThrows(IllegalArgumentException.class, () -> new ThumbnailElement(null));
      +        assertThrows(IllegalArgumentException.class, () -> new ThumbnailElement(null, "image/png", 128, 128));
      +    }
      +
      +    @Test
      +    public void testMinimal() {
      +        ThumbnailElement minimal = new ThumbnailElement("cid:sha1+ffd7c8d28e9c5e82afea41f97108c6b4@bob.xmpp.org");
      +
      +        assertXmlSimilar("",
      +                minimal.toXML());
      +    }
      +
      +    @Test
      +    public void testFull() {
      +        ThumbnailElement full = new ThumbnailElement(
      +                "cid:sha1+ffd7c8d28e9c5e82afea41f97108c6b4@bob.xmpp.org",
      +                "image/png",
      +                128,
      +                96);
      +
      +        assertXmlSimilar("",
      +                full.toXML());
      +    }
      +}
      diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/thumbnails/provider/ThumbnailElementProviderTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/thumbnails/provider/ThumbnailElementProviderTest.java
      new file mode 100644
      index 000000000..dbaec7c9a
      --- /dev/null
      +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/thumbnails/provider/ThumbnailElementProviderTest.java
      @@ -0,0 +1,64 @@
      +/**
      + *
      + * Copyright 2023 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.thumbnails.provider;
      +
      +import static org.junit.jupiter.api.Assertions.assertEquals;
      +import static org.junit.jupiter.api.Assertions.assertNull;
      +
      +import java.io.IOException;
      +
      +import org.jivesoftware.smack.parsing.SmackParsingException;
      +import org.jivesoftware.smack.test.util.SmackTestUtil;
      +import org.jivesoftware.smack.xml.XmlPullParserException;
      +import org.jivesoftware.smackx.thumbnails.element.ThumbnailElement;
      +
      +import org.junit.jupiter.params.ParameterizedTest;
      +import org.junit.jupiter.params.provider.EnumSource;
      +
      +public class ThumbnailElementProviderTest {
      +
      +    @ParameterizedTest
      +    @EnumSource(SmackTestUtil.XmlPullParserKind.class)
      +    public void testParseFull(SmackTestUtil.XmlPullParserKind parserKind) throws XmlPullParserException, IOException, SmackParsingException {
      +        String xml = "";
      +
      +        ThumbnailElement element = SmackTestUtil.parse(xml, ThumbnailElementProvider.class, parserKind);
      +
      +        assertEquals("cid:sha1+ffd7c8d28e9c5e82afea41f97108c6b4@bob.xmpp.org", element.getUri());
      +        assertEquals("image/png", element.getMediaType());
      +        assertEquals(128, element.getWidth());
      +        assertEquals(96, element.getHeight());
      +    }
      +
      +    @ParameterizedTest
      +    @EnumSource(SmackTestUtil.XmlPullParserKind.class)
      +    public void testParseMinimal(SmackTestUtil.XmlPullParserKind parserKind) throws XmlPullParserException, IOException, SmackParsingException {
      +        String xml = "";
      +
      +        ThumbnailElement element = SmackTestUtil.parse(xml, ThumbnailElementProvider.class, parserKind);
      +
      +        assertEquals("cid:sha1+ffd7c8d28e9c5e82afea41f97108c6b4@bob.xmpp.org", element.getUri());
      +        assertNull(element.getMediaType());
      +        assertNull(element.getWidth());
      +        assertNull(element.getHeight());
      +    }
      +}
      diff --git a/smack-extensions/build.gradle b/smack-extensions/build.gradle
      index 2489c9f87..0b62da32c 100644
      --- a/smack-extensions/build.gradle
      +++ b/smack-extensions/build.gradle
      @@ -1,3 +1,8 @@
      +plugins {
      +	id 'org.igniterealtime.smack.java-common-conventions'
      +	id 'org.igniterealtime.smack.android-conventions'
      +}
      +
       description = """\
       Smack extensions.
       Classes and methods that implement support for the various XMPP XEPs
      diff --git a/smack-extensions/src/integration-test/java/org/jivesoftware/smackx/FileTransferTest.java b/smack-extensions/src/integration-test/java/org/jivesoftware/smackx/FileTransferTest.java
      index b3ecc362a..fc9c6d933 100644
      --- a/smack-extensions/src/integration-test/java/org/jivesoftware/smackx/FileTransferTest.java
      +++ b/smack-extensions/src/integration-test/java/org/jivesoftware/smackx/FileTransferTest.java
      @@ -109,7 +109,7 @@ public class FileTransferTest extends SmackTestCase {
                   fail();
               }
               byte [] array = queue.take();
      -        assertEquals("Recieved file not equal to sent file.", testTransfer, array);
      +        assertEquals("Received file not equal to sent file.", testTransfer, array);
           }
       
       
      diff --git a/smack-extensions/src/integration-test/java/org/jivesoftware/smackx/OfflineMessageManagerTest.java b/smack-extensions/src/integration-test/java/org/jivesoftware/smackx/OfflineMessageManagerTest.java
      index 13d4bd23c..5efb0799a 100644
      --- a/smack-extensions/src/integration-test/java/org/jivesoftware/smackx/OfflineMessageManagerTest.java
      +++ b/smack-extensions/src/integration-test/java/org/jivesoftware/smackx/OfflineMessageManagerTest.java
      @@ -50,7 +50,7 @@ public class OfflineMessageManagerTest extends SmackTestCase {
           /**
            * While user2 is connected but unavailable, user1 sends 2 messages to user1. User2 then
            * performs some "Flexible Offline Message Retrieval" checking the number of offline messages,
      -     * retriving the headers, then the real messages of the headers and finally removing the
      +     * retrieving the headers, then the real messages of the headers and finally removing the
            * loaded messages.
            */
           public void testReadAndDelete() {
      diff --git a/smack-extensions/src/integration-test/java/org/jivesoftware/smackx/ServiceDiscoveryManagerTest.java b/smack-extensions/src/integration-test/java/org/jivesoftware/smackx/ServiceDiscoveryManagerTest.java
      index 5d56d85ba..71ba8d7d2 100644
      --- a/smack-extensions/src/integration-test/java/org/jivesoftware/smackx/ServiceDiscoveryManagerTest.java
      +++ b/smack-extensions/src/integration-test/java/org/jivesoftware/smackx/ServiceDiscoveryManagerTest.java
      @@ -90,7 +90,7 @@ public class ServiceDiscoveryManagerTest extends SmackTestCase {
            */
           public void testXHTMLFeature() {
               // Check for local XHTML service support
      -        // By default the XHTML service support is enabled in all the connections
      +        // By default,the XHTML service support is enabled in all the connections
               assertTrue(XHTMLManager.isServiceEnabled(getConnection(0)));
               assertTrue(XHTMLManager.isServiceEnabled(getConnection(1)));
               // Check for XHTML support in connection1 from connection2
      diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/address/packet/MultipleAddresses.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/address/packet/MultipleAddresses.java
      index fba5979ce..6f9e63e81 100644
      --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/address/packet/MultipleAddresses.java
      +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/address/packet/MultipleAddresses.java
      @@ -93,7 +93,7 @@ public class MultipleAddresses implements ExtensionElement {
       
           /**
            * Returns the list of addresses that matches the specified type. Examples of address
      -     * type are: TO, CC, BCC, etc..
      +     * type are: TO, CC, BCC, etc.
            *
            * @param type Examples of address type are: TO, CC, BCC, etc.
            * @return the list of addresses that matches the specified type.
      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/blocking/BlockingCommandManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/BlockingCommandManager.java
      index 6769c9f5b..330cfbc8c 100644
      --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/BlockingCommandManager.java
      +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/BlockingCommandManager.java
      @@ -44,13 +44,13 @@ import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
       import org.jxmpp.jid.Jid;
       
       /**
      - * Block communications with contancts and other entities using XEP-0191.
      + * Block communications with contacts and other entities using XEP-0191.
        * Allows to
        * 
        *
      • Check push notifications support
      • *
      • Get blocking list
      • *
      • Block contact
      • - *
      • Unblock conact
      • + *
      • Unblock contact
      • *
      • Unblock all
      • *
      * diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/element/BlockContactsIQ.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/element/BlockContactsIQ.java index 39cb9525b..79bd2068a 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/element/BlockContactsIQ.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/element/BlockContactsIQ.java @@ -51,6 +51,7 @@ public class BlockContactsIQ extends IQ { * * @param jids TODO javadoc me please */ + @SuppressWarnings("this-escape") public BlockContactsIQ(List jids) { super(ELEMENT, NAMESPACE); this.setType(Type.set); diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/element/UnblockContactsIQ.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/element/UnblockContactsIQ.java index e0a28f722..efba53e3a 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/element/UnblockContactsIQ.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/element/UnblockContactsIQ.java @@ -52,6 +52,7 @@ public class UnblockContactsIQ extends IQ { * * @param jids TODO javadoc me please */ + @SuppressWarnings("this-escape") public UnblockContactsIQ(List jids) { super(ELEMENT, NAMESPACE); this.setType(Type.set); diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/bob/element/BoBIQ.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/bob/element/BoBIQ.java index 085df5fd5..2790e3860 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/bob/element/BoBIQ.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/bob/element/BoBIQ.java @@ -1,6 +1,6 @@ /** * - * Copyright 2016 Fernando Ramirez, 2020 Florian Schmaus + * Copyright 2016 Fernando Ramirez, 2020-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. @@ -65,18 +65,6 @@ public class BoBIQ extends IQ { this(cid, null); } - /** - * Get the BoB hash. - * - * @return the BoB hash - * @deprecated use {@link #getContentId()} instead. - */ - // TODO: Remove in Smack 4.5. - @Deprecated - public ContentId getBoBHash() { - return cid; - } - /** * Get the BoB hash. * diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/bookmarks/BookmarkManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/bookmarks/BookmarkManager.java index fe8323953..9d00ec626 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/bookmarks/BookmarkManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/bookmarks/BookmarkManager.java @@ -103,7 +103,7 @@ public final class BookmarkManager { * * @param name the name of the conference * @param jid the jid of the conference - * @param isAutoJoin whether or not to join this conference automatically on login + * @param isAutoJoin whether to join this conference automatically on login * @param nickname the nickname to use for the user when joining the conference * @param password the password to use for the user when joining the conference * @throws XMPPErrorException thrown when there is an issue retrieving the current bookmarks from @@ -166,7 +166,7 @@ public final class BookmarkManager { * Returns an unmodifiable collection of all bookmarked urls. * * @return returns an unmodifiable collection of all bookmarked urls. - * @throws XMPPErrorException thrown when there is a problem retriving bookmarks from the server. + * @throws XMPPErrorException thrown when there is a problem retrieving bookmarks from the server. * @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. @@ -181,8 +181,8 @@ public final class BookmarkManager { * * @param URL the url of the bookmark * @param name the name of the bookmark - * @param isRSS whether or not the url is an rss feed - * @throws XMPPErrorException thrown when there is an error retriving or saving bookmarks from or to + * @param isRSS whether the url is an RSS feed + * @throws XMPPErrorException thrown when there is an error retrieving or saving bookmarks from or to * the server * @throws NoResponseException if there was no response from the server. * @throws NotConnectedException if the XMPP connection is not connected. @@ -210,7 +210,7 @@ public final class BookmarkManager { * Removes a url from the bookmarks. * * @param bookmarkURL the url of the bookmark to remove - * @throws XMPPErrorException thrown if there is an error retriving or saving bookmarks from or to + * @throws XMPPErrorException thrown if there is an error retrieving or saving bookmarks from or to * the server. * @throws NoResponseException if there was no response from the server. * @throws NotConnectedException if the XMPP connection is not connected. diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamManager.java index 9061a5606..e527d416a 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamManager.java @@ -16,8 +16,8 @@ */ package org.jivesoftware.smackx.bytestreams.ibb; +import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.WeakHashMap; @@ -135,7 +135,7 @@ public final class InBandBytestreamManager extends Manager implements Bytestream * list of listeners that respond to all In-Band Bytestream requests if there are no user * specific listeners for that request */ - private final List allRequestListeners = Collections.synchronizedList(new LinkedList()); + private final List allRequestListeners = Collections.synchronizedList(new ArrayList()); /* listener that handles all incoming In-Band Bytestream requests */ private final InitiationListener initiationListener; @@ -162,7 +162,7 @@ public final class InBandBytestreamManager extends Manager implements Bytestream * list containing session IDs of In-Band Bytestream open packets that should be ignored by the * InitiationListener */ - private final List ignoredBytestreamRequests = Collections.synchronizedList(new LinkedList()); + private final List ignoredBytestreamRequests = Collections.synchronizedList(new ArrayList()); /** * Returns the InBandBytestreamManager to handle In-Band Bytestreams for a given @@ -511,7 +511,7 @@ public final class InBandBytestreamManager extends Manager implements Bytestream } /** - * Returns the list of session IDs that should be ignored by the InitialtionListener + * Returns the list of session IDs that should be ignored by the InitiationListener * * @return list of session IDs */ diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/ibb/packet/Close.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/ibb/packet/Close.java index cdd002590..9147cdd72 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/ibb/packet/Close.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/ibb/packet/Close.java @@ -36,6 +36,7 @@ public class Close extends IQ { * * @param sessionID unique session ID identifying this In-Band Bytestream */ + @SuppressWarnings("this-escape") public Close(String sessionID) { super(ELEMENT, NAMESPACE); if (sessionID == null || "".equals(sessionID)) { diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/ibb/packet/Data.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/ibb/packet/Data.java index 54bbaeb80..77ceb9450 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/ibb/packet/Data.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/ibb/packet/Data.java @@ -34,6 +34,7 @@ public class Data extends IQ { * * @param data data stanza extension containing the encoded data */ + @SuppressWarnings("this-escape") public Data(DataPacketExtension data) { super(DataPacketExtension.ELEMENT, DataPacketExtension.NAMESPACE); if (data == null) { diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/ibb/packet/Open.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/ibb/packet/Open.java index 768cf3f20..a2e54bdaf 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/ibb/packet/Open.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/ibb/packet/Open.java @@ -54,6 +54,7 @@ public class Open extends IQ { * @param blockSize block size in which the data will be fragmented * @param stanza stanza type used to encapsulate the data */ + @SuppressWarnings("this-escape") public Open(String sessionID, int blockSize, StanzaType stanza) { super(ELEMENT, NAMESPACE); if (sessionID == null || "".equals(sessionID)) { diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamManager.java index 673f7a989..fac036a1d 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamManager.java @@ -22,7 +22,6 @@ import java.net.Socket; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; @@ -128,7 +127,7 @@ public final class Socks5BytestreamManager extends Manager implements Bytestream * list of listeners that respond to all bytestream requests if there are not user specific * listeners for that request */ - private final List allRequestListeners = Collections.synchronizedList(new LinkedList()); + private final List allRequestListeners = Collections.synchronizedList(new ArrayList()); /* listener that handles all incoming bytestream requests */ private final InitiationListener initiationListener; @@ -139,7 +138,7 @@ public final class Socks5BytestreamManager extends Manager implements Bytestream /* timeout for connecting to the SOCKS5 proxy selected by the target */ private int proxyConnectionTimeout = 10000; - /* blacklist of errornous SOCKS5 proxies */ + /* blacklist of erroneous SOCKS5 proxies */ private final Set proxyBlacklist = Collections.synchronizedSet(new HashSet()); /* remember the last proxy that worked to prioritize it */ @@ -154,7 +153,7 @@ public final class Socks5BytestreamManager extends Manager implements Bytestream * list containing session IDs of SOCKS5 Bytestream initialization packets that should be * ignored by the InitiationListener */ - private final List ignoredBytestreamRequests = Collections.synchronizedList(new LinkedList()); + private final List ignoredBytestreamRequests = Collections.synchronizedList(new ArrayList()); /** * Returns the Socks5BytestreamManager to handle SOCKS5 Bytestreams for a given @@ -390,7 +389,7 @@ public final class Socks5BytestreamManager extends Manager implements Bytestream } /** - * Set whether or not the bytestream manager will annouce the local stream host(s), i.e. the local SOCKS5 proxy. + * Set whether the bytestream manager will announce the local stream host(s), i.e. the local SOCKS5 proxy. * * @param announceLocalStreamHost TODO javadoc me please * @see #isAnnouncingLocalStreamHostEnabled() @@ -580,7 +579,7 @@ public final class Socks5BytestreamManager extends Manager implements Bytestream proxyInfo = serviceDiscoveryManager.discoverInfo(item.getEntityID()); } catch (NoResponseException | XMPPErrorException e) { - // blacklist errornous server + // blacklist erroneous server proxyBlacklist.add(item.getEntityID()); continue; } @@ -627,7 +626,7 @@ public final class Socks5BytestreamManager extends Manager implements Bytestream streamHosts.addAll(response.getStreamHosts()); } catch (Exception e) { - // blacklist errornous proxies + // blacklist erroneous proxies this.proxyBlacklist.add(proxy); } } @@ -796,7 +795,7 @@ public final class Socks5BytestreamManager extends Manager implements Bytestream } /** - * Returns the list of session IDs that should be ignored by the InitialtionListener + * Returns the list of session IDs that should be ignored by the InitiationListener * * @return list of session IDs */ diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5Client.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5Client.java index 7f80e8260..e4444c6ac 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5Client.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5Client.java @@ -138,7 +138,7 @@ public class Socks5Client { byte[] connectionRequest; byte[] connectionResponse; /* - * use DataInputStream/DataOutpuStream to assure read and write is completed in a single + * use DataInputStream/DataOutputStream to assure read and write is completed in a single * statement */ DataInputStream in = new DataInputStream(socket.getInputStream()); diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5Proxy.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5Proxy.java index 88e7abfe2..117333095 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5Proxy.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5Proxy.java @@ -26,12 +26,12 @@ import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.HashSet; import java.util.LinkedHashSet; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; @@ -107,7 +107,7 @@ public class Socks5Proxy { private final Map connectionMap = new ConcurrentHashMap<>(); /* list of digests connections should be stored */ - private final List allowedConnections = Collections.synchronizedList(new LinkedList()); + private final List allowedConnections = Collections.synchronizedList(new ArrayList()); private final Set localAddresses = new LinkedHashSet<>(4); @@ -149,6 +149,7 @@ public class Socks5Proxy { * * @param serverSocket the server socket to use */ + @SuppressWarnings("this-escape") protected Socks5Proxy(ServerSocket serverSocket) { this.serverProcess = new Socks5ServerProcess(); this.serverSocket = serverSocket; @@ -345,7 +346,7 @@ public class Socks5Proxy { */ public List getLocalAddresses() { synchronized (localAddresses) { - return new LinkedList<>(localAddresses); + return new ArrayList<>(localAddresses); } } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/packet/Bytestream.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/packet/Bytestream.java index f050db26e..bc049a034 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/packet/Bytestream.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/packet/Bytestream.java @@ -68,6 +68,7 @@ public class Bytestream extends IQ { * @param SID The session ID related to the negotiation. * @see #setSessionID(String) */ + @SuppressWarnings("this-escape") public Bytestream(final String SID) { this(); setSessionID(SID); @@ -298,7 +299,7 @@ public class Bytestream extends IQ { * @param port port of the stream host. */ public StreamHost(final Jid jid, final String address, int port) { - this(jid, InternetAddress.from(address), port); + this(jid, InternetAddress.fromIgnoringZoneId(address), port); } public StreamHost(Jid jid, InetAddress address, int port) { 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..aaa87e2b0 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/caps/EntityCapsManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/caps/EntityCapsManager.java @@ -1,6 +1,6 @@ /** * - * Copyright © 2009 Jonas Ådahl, 2011-2022 Florian Schmaus + * Copyright © 2009 Jonas Ådahl, 2011-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. @@ -19,11 +19,12 @@ package org.jivesoftware.smackx.caps; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedList; +import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; @@ -90,7 +91,7 @@ import org.jxmpp.util.cache.LruCache; * but XEP-0030 then XEP-0030 mechanisms are transparently used. *

      *

      - * The caches used by Smack for Entity Capabilities is non-persisent per default. However, it is is also possible to set + * The caches used by Smack for Entity Capabilities is non-persistent per default. However, it is is also possible to set * a persistent Entity Capabilities cache, which is recommended. *

      *

      Examples

      @@ -225,7 +226,7 @@ public final class EntityCapsManager extends Manager { /** * Get the Node version (node#ver) of a JID. Returns a String or null if - * EntiyCapsManager does not have any information. + * EntityCapsManager does not have any information. * * @param jid TODO javadoc me please * the user (Full JID) @@ -388,7 +389,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 @@ -551,7 +555,7 @@ public final class EntityCapsManager extends Manager { if (connection != null) JID_TO_NODEVER_CACHE.put(connection.getUser(), new NodeVerHash(entityNode, currentCapsVersion)); - final List identities = new LinkedList<>(ServiceDiscoveryManager.getInstanceFor(connection).getIdentities()); + final List identities = new ArrayList<>(ServiceDiscoveryManager.getInstanceFor(connection).getIdentities()); sdm.setNodeInformationProvider(localNodeVer, new AbstractNodeInformationProvider() { List features = sdm.getFeatures(); List packetExtensions = sdm.getExtendedInfo(); @@ -698,16 +702,30 @@ public final class EntityCapsManager extends Manager { } List extendedInfos = discoverInfo.getExtensions(DataForm.class); - for (DataForm extendedInfo : extendedInfos) { - if (!extendedInfo.hasHiddenFormTypeField()) { + final Iterator iter = extendedInfos.iterator(); + while (iter.hasNext()) { + if (!iter.next().hasHiddenFormTypeField()) { // Only use the data form for calculation is it has a hidden FORM_TYPE field. // See XEP-0115 5.4 step 3.f - continue; + iter.remove(); } + } - // 6. If the service discovery information response includes - // XEP-0128 data forms, sort the forms by the FORM_TYPE (i.e., - // by the XML character data of the element). + // 6. If the service discovery information response includes + // XEP-0128 data forms, sort the forms by the FORM_TYPE (i.e., + // by the XML character data of the element). + Collections.sort(extendedInfos, new Comparator() { + @Override + public int compare(DataForm dataFormLeft, DataForm dataFormRight) { + final String formTypeLeft = dataFormLeft.getFormType(); + assert formTypeLeft != null; // ensured by the previous step. + final String formTypeRight = dataFormRight.getFormType(); + assert formTypeRight != null; // ensured by the previous step. + return formTypeLeft.compareTo(formTypeRight); + } + }); + + for (DataForm extendedInfo : extendedInfos) { SortedSet fs = new TreeSet<>(new Comparator() { @Override public int compare(FormField f1, FormField f2) { 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..5eb6d6dda --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/AdHocCommandHandler.java @@ -0,0 +1,182 @@ +/** + * + * 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; + + @SuppressWarnings("this-escape") + 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..568b98a14 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 @@ -82,7 +85,7 @@ public final class AdHocCommandManager extends Manager { */ private static final Map instances = new WeakHashMap<>(); - /** + /* * Register the listener for all the connection creations. When a new * connection is created a new AdHocCommandManager is also created and * related to that connection. @@ -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 clazz) { - registerCommand(node, name, new LocalCommandFactory() { + public void registerCommand(String node, String name, final Class clazz) throws NoSuchMethodException, SecurityException { + Constructor 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 * 1000L) { + // 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() * 1000L) { // 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 ConcurrentHashMap.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/delay/filter/DelayedStanzaFilter.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/delay/filter/DelayedStanzaFilter.java index 054ed42cf..90c30f563 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/delay/filter/DelayedStanzaFilter.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/delay/filter/DelayedStanzaFilter.java @@ -23,14 +23,14 @@ import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smackx.delay.DelayInformationManager; /** - * Filters stanza with delay information, ie. stanzas that got delayed for some reason + * Filters stanza with delay information, i.e. stanzas that got delayed for some reason */ public final class DelayedStanzaFilter implements StanzaFilter { public static final StanzaFilter INSTANCE = new DelayedStanzaFilter(); /** - * Filters stanzas that got not delayed, ie. have no delayed information + * Filters stanzas that got not delayed, i.e. have no delayed information */ public static final StanzaFilter NOT_DELAYED_STANZA = new NotFilter(INSTANCE); diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/ServiceDiscoveryManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/ServiceDiscoveryManager.java index 6e43fbc9a..e50f6d956 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/ServiceDiscoveryManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/ServiceDiscoveryManager.java @@ -1,6 +1,6 @@ /** * - * Copyright 2003-2007 Jive Software, 2018-2022 Florian Schmaus. + * Copyright 2003-2007 Jive Software, 2018-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. @@ -22,7 +22,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; @@ -294,7 +293,7 @@ public final class ServiceDiscoveryManager extends Manager { /** * Returns all identities of this client as unmodifiable Collection. * - * @return all identies as set + * @return all identities as a set */ public Set getIdentities() { Set res = new HashSet<>(identities); @@ -442,27 +441,6 @@ public final class ServiceDiscoveryManager extends Manager { return features.contains(feature); } - /** - * Registers extended discovery information of this XMPP entity. When this - * client is queried for its information this data form will be returned as - * specified by XEP-0128. - *

    - * - * Since no stanza is actually sent to the server it is safe to perform this - * operation before logging to the server. In fact, you may want to - * configure the extended info before logging to the server so that the - * information is already available if it is required upon login. - * - * @param info the data form that contains the extend service discovery - * information. - * @deprecated use {@link #addExtendedInfo(DataForm)} instead. - */ - // TODO: Remove in Smack 4.5 - @Deprecated - public synchronized void setExtendedInfo(DataForm info) { - addExtendedInfo(info); - } - /** * Registers extended discovery information of this XMPP entity. When this * client is queried for its information this data form will be returned as @@ -518,19 +496,6 @@ public final class ServiceDiscoveryManager extends Manager { return CollectionUtil.newListWith(extendedInfos); } - /** - * Returns the data form as List of PacketExtensions, or null if no data form is set. - * This representation is needed by some classes (e.g. EntityCapsManager, NodeInformationProvider) - * - * @return the data form as List of PacketExtensions - * @deprecated use {@link #getExtendedInfo()} instead. - */ - // TODO: Remove in Smack 4.5 - @Deprecated - public List getExtendedInfoAsList() { - return getExtendedInfo(); - } - /** * Removes the data form containing extended service discovery information * from the information returned by this XMPP entity.

    @@ -806,7 +771,7 @@ public final class ServiceDiscoveryManager extends Manager { return serviceDiscoInfo; } } - serviceDiscoInfo = new LinkedList<>(); + serviceDiscoInfo = new ArrayList<>(); // Send the disco packet to the server itself DiscoverInfo info; try { diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/packet/DiscoverInfo.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/packet/DiscoverInfo.java index abe18f630..9df5c64ac 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/packet/DiscoverInfo.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/packet/DiscoverInfo.java @@ -17,10 +17,8 @@ package org.jivesoftware.smackx.disco.packet; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.HashSet; -import java.util.LinkedList; import java.util.List; import java.util.Set; @@ -83,17 +81,6 @@ public class DiscoverInfo extends IQ implements DiscoverInfoView { } } - /** - * Deprecated. - * - * @deprecated use {@link DiscoverInfoBuilder} instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public DiscoverInfo() { - super(ELEMENT, NAMESPACE); - } - /** * Copy constructor. * @@ -114,85 +101,11 @@ public class DiscoverInfo extends IQ implements DiscoverInfoView { identitiesSet.addAll(d.identitiesSet); } - /** - * Adds a new feature to the discovered information. - * - * @param feature the discovered feature - * @return true if the feature did not already exist. - * @deprecated use {@link DiscoverInfoBuilder#addFeature(String)} instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public boolean addFeature(String feature) { - return addFeature(new Feature(feature)); - } - - /** - * Adds a collection of features to the packet. Does noting if featuresToAdd is null. - * - * @param featuresToAdd TODO javadoc me please - * @deprecated use {@link DiscoverInfoBuilder#addFeatures(Collection)} instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public void addFeatures(Collection featuresToAdd) { - if (featuresToAdd == null) return; - for (String feature : featuresToAdd) { - addFeature(feature); - } - } - - /** - * Deprecated. - * - * @param feature the future. - * @return true if the feature is new. - * @deprecated use {@link DiscoverInfoBuilder#addFeature(DiscoverInfo.Feature)} instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public boolean addFeature(Feature feature) { - features.add(feature); - boolean featureIsNew = featuresSet.add(feature); - if (!featureIsNew) { - containsDuplicateFeatures = true; - } - return featureIsNew; - } - @Override public List getFeatures() { return Collections.unmodifiableList(features); } - /** - * Adds a new identity of the requested entity to the discovered information. - * - * @param identity the discovered entity's identity - * @deprecated use {@link DiscoverInfoBuilder#addIdentity(DiscoverInfo.Identity)} instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public void addIdentity(Identity identity) { - identities.add(identity); - identitiesSet.add(identity.getKey()); - } - - /** - * Adds identities to the DiscoverInfo stanza. - * - * @param identitiesToAdd TODO javadoc me please - * @deprecated use {@link DiscoverInfoBuilder#addIdentities(Collection)} instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public void addIdentities(Collection identitiesToAdd) { - if (identitiesToAdd == null) return; - for (Identity identity : identitiesToAdd) { - addIdentity(identity); - } - } - @Override public List getIdentities() { return Collections.unmodifiableList(identities); @@ -215,7 +128,7 @@ public class DiscoverInfo extends IQ implements DiscoverInfoView { * * @param category category the category to look for. * @param type type the type to look for. - * @return a list of Identites with the given category and type. + * @return a list of Identities with the given category and type. */ public List getIdentities(String category, String type) { List res = new ArrayList<>(identities.size()); @@ -232,22 +145,6 @@ public class DiscoverInfo extends IQ implements DiscoverInfoView { return node; } - /** - * Sets the node attribute that supplements the 'jid' attribute. A node is merely - * something that is associated with a JID and for which the JID can provide information.

    - * - * Node attributes SHOULD be used only when trying to provide or query information which - * is not directly addressable. - * - * @param node the node attribute that supplements the 'jid' attribute - * @deprecated use {@link DiscoverInfoBuilder#setNode(String)} instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public void setNode(String node) { - this.node = StringUtils.requireNullOrNotEmpty(node, "The node can not be the empty string"); - } - /** * Returns true if the specified feature is part of the discovered information. * @@ -286,7 +183,7 @@ public class DiscoverInfo extends IQ implements DiscoverInfoView { * @return true if duplicate identities where found, otherwise false */ public boolean containsDuplicateIdentities() { - List checkedIdentities = new LinkedList<>(); + List checkedIdentities = new ArrayList<>(identities.size()); for (Identity i : identities) { for (Identity i2 : checkedIdentities) { if (i.equals(i2)) @@ -310,18 +207,6 @@ public class DiscoverInfo extends IQ implements DiscoverInfoView { return new DiscoverInfoBuilder(this, stanzaId); } - /** - * Deprecated, do not use. - * - * @deprecated use {@link #asBuilder(String)} instead. - */ - // TODO: Remove in Smack 4.5. - @Deprecated - @Override - public DiscoverInfo clone() { - return new DiscoverInfo(this); - } - public static DiscoverInfoBuilder builder(XMPPConnection connection) { return new DiscoverInfoBuilder(connection); } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/packet/DiscoverItems.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/packet/DiscoverItems.java index 34c62a8a1..a6394716c 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/packet/DiscoverItems.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/packet/DiscoverItems.java @@ -16,9 +16,9 @@ */ package org.jivesoftware.smackx.disco.packet; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.LinkedList; import java.util.List; import org.jivesoftware.smack.packet.IQ; @@ -40,7 +40,7 @@ public class DiscoverItems extends IQ { public static final String ELEMENT = QUERY_ELEMENT; public static final String NAMESPACE = "http://jabber.org/protocol/disco#items"; - private final List items = new LinkedList<>(); + private final List items = new ArrayList<>(); private String node; public DiscoverItems() { diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/filetransfer/IncomingFileTransfer.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/filetransfer/IncomingFileTransfer.java index c09e9fcb2..9a17438d0 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/filetransfer/IncomingFileTransfer.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/filetransfer/IncomingFileTransfer.java @@ -51,7 +51,7 @@ import org.jivesoftware.smack.util.CloseableUtil; * The second way that a file can be received through this class is by invoking * the {@link #receiveFile(File)} method. This method returns immediately and * takes as its parameter a file on the local file system where the file - * recieved from the transfer will be put. + * received from the transfer will be put. * * @author Alexander Wenckus */ diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/filetransfer/StreamNegotiator.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/filetransfer/StreamNegotiator.java index adf7297e5..0fc025827 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/filetransfer/StreamNegotiator.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/filetransfer/StreamNegotiator.java @@ -52,7 +52,7 @@ public abstract class StreamNegotiator extends Manager { } /** - * A event manager for stream initiation requests send to us. + * An event manager for stream initiation requests send to us. *

    * Those are typical XEP-45 Open or XEP-65 Bytestream IQ requests. The even key is in the format * "initiationFrom + '\t' + streamId" diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/filetransfer/package-info.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/filetransfer/package-info.java index 505394d1e..a05f6d56c 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/filetransfer/package-info.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/filetransfer/package-info.java @@ -144,7 +144,7 @@ *

  • **isDone()** - Similar to getProgress() except it returns a _boolean_. If the state is rejected, canceled, error, * or complete then true will be returned and false otherwise.
  • *
  • **getError()** - If there is an error during the file transfer this method will return the type of error that - * occured.
  • + * occurred. * *

    Examples

    *

    diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/formtypes/FormFieldRegistry.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/formtypes/FormFieldRegistry.java index 5dda27842..9dae0e1d5 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/formtypes/FormFieldRegistry.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/formtypes/FormFieldRegistry.java @@ -94,7 +94,13 @@ public class FormFieldRegistry { } else { previousType = fieldNameToType.get(fieldName); if (previousType != null && previousType != fieldType) { - throw new IllegalArgumentException(); + String message = "The field '" + fieldName + "' from form type '" + formType + + "' was already registered with field type '" + previousType + + "' while it is now seen with type '" + fieldType + + "'. Form field types have to be unambigiously." + + " XMPP uses a registry for form field tpes, scoped by the form type." + + " You may find the correct value at https://xmpp.org/registrar/formtypes.html"; + throw new IllegalArgumentException(message); } } previousType = fieldNameToType.put(fieldName, fieldType); diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/geoloc/GeoLocationManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/geoloc/GeoLocationManager.java index f29306251..eb91bec55 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/geoloc/GeoLocationManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/geoloc/GeoLocationManager.java @@ -43,7 +43,7 @@ import org.jxmpp.jid.Jid; *
    * To publish a UserLocation, please use {@link #publishGeoLocation(GeoLocation)} method. This will publish the node. *
    - * To stop publishing a UserLocation, please use {@link #stopPublishingGeolocation()} method. This will send a disble publishing signal. + * To stop publishing a UserLocation, please use {@link #stopPublishingGeolocation()} method. This will send a disable publishing signal. *
    * To add a {@link PepEventListener} in order to remain updated with other users GeoLocation, use {@link #addGeoLocationListener(PepEventListener)} method. *
    diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/iqlast/packet/LastActivity.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/iqlast/packet/LastActivity.java index 08d3ac686..9a94d0198 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/iqlast/packet/LastActivity.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/iqlast/packet/LastActivity.java @@ -45,11 +45,13 @@ public class LastActivity extends IQ { public long lastActivity = -1; public String message; + @SuppressWarnings("this-escape") public LastActivity() { super(ELEMENT, NAMESPACE); setType(IQ.Type.get); } + @SuppressWarnings("this-escape") public LastActivity(Jid to) { this(); setTo(to); diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/iqprivate/packet/PrivateData.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/iqprivate/packet/PrivateData.java index 058b0c2d5..c6670c900 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/iqprivate/packet/PrivateData.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/iqprivate/packet/PrivateData.java @@ -41,7 +41,7 @@ public interface PrivateData { String getNamespace(); /** - * Returns the XML reppresentation of the PrivateData. + * Returns the XML representation of the PrivateData. * * @return the private data as XML. */ diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/iqprivate/packet/PrivateDataIQ.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/iqprivate/packet/PrivateDataIQ.java index 6063e040e..5df357e09 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/iqprivate/packet/PrivateDataIQ.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/iqprivate/packet/PrivateDataIQ.java @@ -1,6 +1,6 @@ /** * - * Copyright © 2014 Florian Schmaus + * Copyright 2014-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. @@ -27,11 +27,13 @@ public class PrivateDataIQ extends IQ { private final String getElement; private final String getNamespace; + @SuppressWarnings("this-escape") public PrivateDataIQ(PrivateData privateData) { this(privateData, null, null); setType(Type.set); } + @SuppressWarnings("this-escape") public PrivateDataIQ(String element, String namespace) { this(null, element, namespace); setType(Type.get); diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/iqregister/AccountManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/iqregister/AccountManager.java index 7a991978c..c6f32970c 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/iqregister/AccountManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/iqregister/AccountManager.java @@ -354,15 +354,17 @@ public final class AccountManager extends Manager { /** * Gets the account registration info from the server. * + * @return Registration information * @throws XMPPErrorException if there was an XMPP error returned. * @throws NoResponseException if there was no response from the remote entity. * @throws NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. */ - private synchronized void getRegistrationInfo() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { + public synchronized Registration getRegistrationInfo() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { Registration reg = new Registration(); reg.setTo(connection().getXMPPServiceDomain()); info = createStanzaCollectorAndSend(reg).nextResultOrThrow(); + return info; } private StanzaCollector createStanzaCollectorAndSend(IQ req) throws NotConnectedException, InterruptedException { diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/iqregister/provider/RegistrationProvider.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/iqregister/provider/RegistrationProvider.java index 67da11bbe..d087a5bf4 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/iqregister/provider/RegistrationProvider.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/iqregister/provider/RegistrationProvider.java @@ -17,8 +17,8 @@ package org.jivesoftware.smackx.iqregister.provider; import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; -import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -40,7 +40,7 @@ public class RegistrationProvider extends IqProvider { public Registration parse(XmlPullParser parser, int initialDepth, IqData iqData, XmlEnvironment xmlEnvironment) throws XmlPullParserException, IOException, SmackParsingException { String instruction = null; Map fields = new HashMap<>(); - List packetExtensions = new LinkedList<>(); + List packetExtensions = new ArrayList<>(); outerloop: while (true) { XmlPullParser.Event eventType = parser.next(); diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/iqversion/packet/Version.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/iqversion/packet/Version.java index b0dd5c1d0..fb0ca01fa 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/iqversion/packet/Version.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/iqversion/packet/Version.java @@ -1,6 +1,6 @@ /** * - * Copyright 2003-2007 Jive Software, 2021 Florian Schmaus. + * Copyright 2003-2007 Jive Software, 2021-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. @@ -24,7 +24,7 @@ import org.jivesoftware.smack.util.StringUtils; /** * A Version IQ packet, which is used by XMPP clients to discover version information - * about the software running at another entity's JID.

    + * about the software running at another entity's JID. * * @author Gaston Dombiak */ diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/element/Jingle.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/element/Jingle.java index ea36bcc31..4fb09922c 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/element/Jingle.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/element/Jingle.java @@ -52,7 +52,7 @@ import org.jxmpp.jid.FullJid; * | transport-info * | transport-reject * | transport-replace - * │ initator (RECOMMENDED for session initiate, NOT RECOMMENDED otherwise, full JID, XEP-0166 § 7.1) + * │ initiator (RECOMMENDED for session initiate, NOT RECOMMENDED otherwise, full JID, XEP-0166 § 7.1) * │ responder (RECOMMENDED for session accept, NOT RECOMMENDED otherwise, full JID. XEP-0166 § 7.1) * │ sid (REQUIRED, SHOULD match XML Nmtoken production) * │ diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/element/JingleContent.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/element/JingleContent.java index ab5680c1d..702183dd2 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/element/JingleContent.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/element/JingleContent.java @@ -71,7 +71,7 @@ public final class JingleContent implements XmlElement { private final JingleContentTransport transport; /** - * Creates a content description.. + * Creates a content description. */ private JingleContent(Creator creator, String disposition, String name, Senders senders, JingleContentDescription description, JingleContentTransport transport) { diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/transports/JingleTransportManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/transports/JingleTransportManager.java index 8c91c8976..ba344ee7c 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/transports/JingleTransportManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/transports/JingleTransportManager.java @@ -30,6 +30,7 @@ public abstract class JingleTransportManager i private final XMPPConnection connection; + @SuppressWarnings("this-escape") public JingleTransportManager(XMPPConnection connection) { this.connection = connection; connection.addConnectionListener(this); diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/transports/jingle_s5b/elements/JingleS5BTransportCandidate.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/transports/jingle_s5b/elements/JingleS5BTransportCandidate.java index 4ad96fcca..d3be5a4b3 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/transports/jingle_s5b/elements/JingleS5BTransportCandidate.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/transports/jingle_s5b/elements/JingleS5BTransportCandidate.java @@ -51,7 +51,7 @@ public final class JingleS5BTransportCandidate extends JingleContentTransportCan private final Type type; public JingleS5BTransportCandidate(String candidateId, String hostString, Jid jid, int port, int priority, Type type) { - this(candidateId, InternetAddress.from(hostString), jid, port, priority, type); + this(candidateId, InternetAddress.fromIgnoringZoneId(hostString), jid, port, priority, type); } public JingleS5BTransportCandidate(String candidateId, InternetAddress host, Jid jid, int port, int priority, Type type) { @@ -176,7 +176,7 @@ public final class JingleS5BTransportCandidate extends JingleContentTransportCan } public Builder setHost(String host) { - InternetAddress inetAddress = InternetAddress.from(host); + InternetAddress inetAddress = InternetAddress.fromIgnoringZoneId(host); return setHost(inetAddress); } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/jiveproperties/JivePropertiesManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/jiveproperties/JivePropertiesManager.java index f02f56c8e..09bf70c5b 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/jiveproperties/JivePropertiesManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/jiveproperties/JivePropertiesManager.java @@ -1,6 +1,6 @@ /** * - * Copyright 2014 Florian Schmaus. + * Copyright 2014-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. @@ -50,25 +50,6 @@ public class JivePropertiesManager { return javaObjectEnabled; } - /** - * Convenience method to add a property to a packet. - * - * @param packet the stanza to add the property to. - * @param name the name of the property to add. - * @param value the value of the property to add. - * @deprecated use {@link #addProperty(StanzaBuilder, String, Object)} instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public static void addProperty(Stanza packet, String name, Object value) { - JivePropertiesExtension jpe = (JivePropertiesExtension) packet.getExtension(JivePropertiesExtension.NAMESPACE); - if (jpe == null) { - jpe = new JivePropertiesExtension(); - packet.addExtension(jpe); - } - jpe.setProperty(name, value); - } - /** * Convenience method to add a property to a stanza. * diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/jiveproperties/package-info.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/jiveproperties/package-info.java index 45a9fe3e9..a5ea271a5 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/jiveproperties/package-info.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/jiveproperties/package-info.java @@ -70,7 +70,7 @@ chat.sendMessage(message); *

      *
    • When you send a Java object as a property, only clients running Java will be able to interpret the data. So, * consider using a series of primitive values to transfer data instead.
    • - *
    • Objects sent as property values must implement Serialiable. Additionally, both the sender and receiver must have + *
    • Objects sent as property values must implement Serializable. Additionally, both the sender and receiver must have * identical versions of the class, or a serialization exception will occur when de-serializing the object.
    • *
    • Serialized objects can potentially be quite large, which will use more bandwidth and server resources.
    • *
    diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/jiveproperties/provider/JivePropertiesExtensionProvider.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/jiveproperties/provider/JivePropertiesExtensionProvider.java index 6d99c1637..8326e93e5 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/jiveproperties/provider/JivePropertiesExtensionProvider.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/jiveproperties/provider/JivePropertiesExtensionProvider.java @@ -46,7 +46,7 @@ public class JivePropertiesExtensionProvider extends ExtensionElementProvider - * Note that you have to explicitly enabled Java object deserialization with @{link + * Note that you have to explicitly enabled Java object deserialization with * {@link JivePropertiesManager#setJavaObjectEnabled(boolean)} * * @param parser the XML parser, positioned at the start of a properties sub-packet. diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/last_interaction/element/IdleElement.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/last_interaction/element/IdleElement.java index 2799c1224..6603fe21b 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/last_interaction/element/IdleElement.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/last_interaction/element/IdleElement.java @@ -37,6 +37,7 @@ public class IdleElement implements ExtensionElement { /** * Create a new IdleElement with the current date as date of last user interaction. */ + @SuppressWarnings("JavaUtilDate") public IdleElement() { this(new Date()); } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/Affiliate.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/Affiliate.java index 3e3648f26..d3e034c6c 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/Affiliate.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/Affiliate.java @@ -55,10 +55,10 @@ public class Affiliate { } /** - * Returns the affiliation of the afffiliated user. Possible affiliations are: "owner", "admin", + * Returns the affiliation of the affiliated user. Possible affiliations are: "owner", "admin", * "member", "outcast". This information will always be available. * - * @return the affiliation of the afffiliated user. + * @return the affiliation of the affiliated user. */ public MUCAffiliation getAffiliation() { return affiliation; diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/DefaultParticipantStatusListener.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/DefaultParticipantStatusListener.java deleted file mode 100644 index c9d809cd2..000000000 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/DefaultParticipantStatusListener.java +++ /dev/null @@ -1,98 +0,0 @@ -/** - * - * Copyright 2003-2007 Jive Software. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.jivesoftware.smackx.muc; - -import org.jxmpp.jid.EntityFullJid; -import org.jxmpp.jid.Jid; -import org.jxmpp.jid.parts.Resourcepart; - -/** - * Default implementation of the ParticipantStatusListener interface.

    - * - * This class does not provide any behavior by default. It just avoids having - * to implement all the inteface methods if the user is only interested in implementing - * some of the methods. - * - * @author Gaston Dombiak - * @deprecated use {@link ParticipantStatusListener} instead. - */ -// TODO: Remove in Smack 4.5 -@Deprecated -public class DefaultParticipantStatusListener implements ParticipantStatusListener { - - @Override - public void joined(EntityFullJid participant) { - } - - @Override - public void left(EntityFullJid participant) { - } - - @Override - public void kicked(EntityFullJid participant, Jid actor, String reason) { - } - - @Override - public void voiceGranted(EntityFullJid participant) { - } - - @Override - public void voiceRevoked(EntityFullJid participant) { - } - - @Override - public void banned(EntityFullJid participant, Jid actor, String reason) { - } - - @Override - public void membershipGranted(EntityFullJid participant) { - } - - @Override - public void membershipRevoked(EntityFullJid participant) { - } - - @Override - public void moderatorGranted(EntityFullJid participant) { - } - - @Override - public void moderatorRevoked(EntityFullJid participant) { - } - - @Override - public void ownershipGranted(EntityFullJid participant) { - } - - @Override - public void ownershipRevoked(EntityFullJid participant) { - } - - @Override - public void adminGranted(EntityFullJid participant) { - } - - @Override - public void adminRevoked(EntityFullJid participant) { - } - - @Override - public void nicknameChanged(EntityFullJid participant, Resourcepart newNickname) { - } - -} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/DefaultUserStatusListener.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/DefaultUserStatusListener.java deleted file mode 100644 index 0ac93ccbd..000000000 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/DefaultUserStatusListener.java +++ /dev/null @@ -1,88 +0,0 @@ -/** - * - * Copyright 2003-2007 Jive Software. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.jivesoftware.smackx.muc; - -import org.jxmpp.jid.Jid; - -/** - * Default implementation of the UserStatusListener interface.

    - * - * This class does not provide any behavior by default. It just avoids having - * to implement all the inteface methods if the user is only interested in implementing - * some of the methods. - * - * @author Gaston Dombiak - * @deprecated use {@link UserStatusListener} instead. - */ -// TODO: Remove in Smack 4.5. -@Deprecated -public class DefaultUserStatusListener implements UserStatusListener { - - @Override - public void kicked(Jid actor, String reason) { - } - - @Override - public void voiceGranted() { - } - - @Override - public void voiceRevoked() { - } - - @Override - public void banned(Jid actor, String reason) { - } - - @Override - public void membershipGranted() { - } - - @Override - public void membershipRevoked() { - } - - @Override - public void moderatorGranted() { - } - - @Override - public void moderatorRevoked() { - } - - @Override - public void ownershipGranted() { - } - - @Override - public void ownershipRevoked() { - } - - @Override - public void adminGranted() { - } - - @Override - public void adminRevoked() { - } - - @Override - public void roomDestroyed(MultiUserChat alternateMUC, String reason) { - } - -} 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/MucConfigFormManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MucConfigFormManager.java index 1735d0b4f..b847cd025 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MucConfigFormManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MucConfigFormManager.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2020 Florian Schmaus + * Copyright 2015-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. @@ -25,6 +25,7 @@ import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.XMPPException.XMPPErrorException; import org.jivesoftware.smackx.muc.MultiUserChatException.MucConfigurationNotSupportedException; +import org.jivesoftware.smackx.xdata.BooleanFormField; import org.jivesoftware.smackx.xdata.FormField; import org.jivesoftware.smackx.xdata.form.FillableForm; import org.jivesoftware.smackx.xdata.form.FilledForm; @@ -61,6 +62,13 @@ public class MucConfigFormManager { */ public static final String MUC_ROOMCONFIG_ROOMOWNERS = "muc#roomconfig_roomowners"; + /** + * The constant String {@value}. + * + * @see XEP-0045 § 10. Owner Use Cases + */ + public static final String MUC_ROOMCONFIG_ROOMADMINS = "muc#roomconfig_roomadmins"; + /** * The constant String {@value}. */ @@ -88,10 +96,25 @@ public class MucConfigFormManager { */ public static final String MUC_ROOMCONFIG_PUBLICLYSEARCHABLEROOM = "muc#roomconfig_publicroom"; + /** + * The constant String {@value}. + */ + public static final String MUC_ROOMCONFIG_ROOMNAME = "muc#roomconfig_roomname"; + + /** + * The constant String {@value}. + */ + public static final String MUC_ROOMCONFIG_ENABLE_PUBLIC_LOGGING = "muc#roomconfig_enablelogging"; + + /** + * The constant String {@value}. + */ + public static final String MUC_ROOMCONFIG_CHANGE_SUBJECT = "muc#roomconfig_changesubject"; private final MultiUserChat multiUserChat; private final FillableForm answerForm; private final List owners; + private final List admins; /** * Create a new MUC config form manager. @@ -125,6 +148,18 @@ public class MucConfigFormManager { // roomowners not supported, this should barely be the case owners = null; } + + FormField roomAdminsFormField = answerForm.getDataForm().getField(MUC_ROOMCONFIG_ROOMADMINS); + if (roomAdminsFormField != null) { + // Set 'admins' to the currently configured admins + List adminStrings = roomAdminsFormField.getValues(); + admins = new ArrayList<>(adminStrings.size()); + JidUtil.jidsFrom(adminStrings, admins, null); + } + else { + // roomadmins not supported, this should barely be the case + admins = null; + } } /** @@ -136,6 +171,15 @@ public class MucConfigFormManager { return owners != null; } + /** + * Check if the room supports room admins. + * @return true if supported, false if not. + * @see #MUC_ROOMCONFIG_ROOMADMINS + */ + public boolean supportsRoomAdmins() { + return admins != null; + } + /** * Set the owners of the room. * @@ -153,6 +197,23 @@ public class MucConfigFormManager { return this; } + /** + * Set the admins of the room. + * + * @param newAdmins a collection of JIDs to become the new admins of the room. + * @return a reference to this object. + * @throws MucConfigurationNotSupportedException if the MUC service does not support this option. + * @see #MUC_ROOMCONFIG_ROOMADMINS + */ + public MucConfigFormManager setRoomAdmins(Collection newAdmins) throws MucConfigurationNotSupportedException { + if (!supportsRoomAdmins()) { + throw new MucConfigurationNotSupportedException(MUC_ROOMCONFIG_ROOMADMINS); + } + admins.clear(); + admins.addAll(newAdmins); + return this; + } + /** * Check if the room supports a members only configuration. * @@ -223,6 +284,15 @@ public class MucConfigFormManager { } + /** + * Check if the room supports its visibility being controlled via configuration. + * + * @return true if supported, false if not. + */ + public boolean supportsPublicRoom() { + return answerForm.hasField(MUC_ROOMCONFIG_PUBLICLYSEARCHABLEROOM); + } + /** * Make the room publicly searchable. * @@ -251,13 +321,25 @@ public class MucConfigFormManager { * @throws MucConfigurationNotSupportedException if the requested MUC configuration is not supported by the MUC service. */ public MucConfigFormManager setPublic(boolean isPublic) throws MucConfigurationNotSupportedException { - if (!supportsModeration()) { + if (!supportsPublicRoom()) { throw new MucConfigurationNotSupportedException(MUC_ROOMCONFIG_PUBLICLYSEARCHABLEROOM); } answerForm.setAnswer(MUC_ROOMCONFIG_PUBLICLYSEARCHABLEROOM, isPublic); return this; } + public boolean supportsRoomname() { + return answerForm.hasField(MUC_ROOMCONFIG_ROOMNAME); + } + + public MucConfigFormManager setRoomName(String roomName) throws MucConfigurationNotSupportedException { + if (!supportsRoomname()) { + throw new MucConfigurationNotSupportedException(MUC_ROOMCONFIG_ROOMNAME); + } + answerForm.setAnswer(MUC_ROOMCONFIG_ROOMNAME, roomName); + return this; + } + /** * Check if the room supports password protection. * @@ -299,13 +381,33 @@ public class MucConfigFormManager { */ public MucConfigFormManager setIsPasswordProtected(boolean isPasswordProtected) throws MucConfigurationNotSupportedException { - if (!supportsMembersOnly()) { + if (!supportsPasswordProtected()) { throw new MucConfigurationNotSupportedException(MUC_ROOMCONFIG_PASSWORDPROTECTEDROOM); } answerForm.setAnswer(MUC_ROOMCONFIG_PASSWORDPROTECTEDROOM, isPasswordProtected); return this; } + public boolean supportsPublicLogging() { + return answerForm.hasField(MUC_ROOMCONFIG_ENABLE_PUBLIC_LOGGING); + } + + public MucConfigFormManager setPublicLogging(boolean enabled) throws MucConfigurationNotSupportedException { + if (!supportsPublicLogging()) { + throw new MucConfigurationNotSupportedException(MUC_ROOMCONFIG_ENABLE_PUBLIC_LOGGING); + } + answerForm.setAnswer(MUC_ROOMCONFIG_ENABLE_PUBLIC_LOGGING, enabled); + return this; + } + + public MucConfigFormManager enablePublicLogging() throws MucConfigurationNotSupportedException { + return setPublicLogging(true); + } + + public MucConfigFormManager disablPublicLogging() throws MucConfigurationNotSupportedException { + return setPublicLogging(false); + } + /** * Set the room secret, aka the room password. If set and enabled, the password is required to * join the room. Note that this does only set it by does not enable password protection. Use @@ -324,6 +426,33 @@ public class MucConfigFormManager { return this; } + public boolean supportsChangeSubjectByOccupant() { + return answerForm.hasField(MUC_ROOMCONFIG_CHANGE_SUBJECT); + } + + public boolean occupantsAreAllowedToChangeSubject() throws MucConfigurationNotSupportedException { + if (!supportsChangeSubjectByOccupant()) { + throw new MucConfigurationNotSupportedException(MUC_ROOMCONFIG_CHANGE_SUBJECT); + } + return answerForm.getField(MUC_ROOMCONFIG_CHANGE_SUBJECT).ifPossibleAsOrThrow(BooleanFormField.class).getValueAsBoolean(); + } + + public MucConfigFormManager setChangeSubjectByOccupant(boolean enabled) throws MucConfigurationNotSupportedException { + if (!supportsChangeSubjectByOccupant()) { + throw new MucConfigurationNotSupportedException(MUC_ROOMCONFIG_CHANGE_SUBJECT); + } + answerForm.setAnswer(MUC_ROOMCONFIG_CHANGE_SUBJECT, enabled); + return this; + } + + public MucConfigFormManager allowOccupantsToChangeSubject() throws MucConfigurationNotSupportedException { + return setChangeSubjectByOccupant(true); + } + + public MucConfigFormManager disallowOccupantsToChangeSubject() throws MucConfigurationNotSupportedException { + return setChangeSubjectByOccupant(false); + } + /** * Submit the configuration as {@link FilledForm} to the room. * @@ -337,6 +466,9 @@ public class MucConfigFormManager { if (owners != null) { answerForm.setAnswer(MUC_ROOMCONFIG_ROOMOWNERS, JidUtil.toStringList(owners)); } + if (admins != null) { + answerForm.setAnswer(MUC_ROOMCONFIG_ROOMADMINS, JidUtil.toStringList(admins)); + } multiUserChat.sendConfigurationForm(answerForm); } } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MucEnterConfiguration.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MucEnterConfiguration.java index ef3e17ba7..58c0c4e26 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MucEnterConfiguration.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MucEnterConfiguration.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2020 Florian Schmaus + * Copyright 2015-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. @@ -60,13 +60,8 @@ public final class MucEnterConfiguration { since = builder.since; timeout = builder.timeout; - final PresenceBuilder joinPresenceBuilder; - if (builder.joinPresence == null) { - joinPresenceBuilder = builder.joinPresenceBuilder.ofType(Presence.Type.available); - } - else { - joinPresenceBuilder = builder.joinPresence.asBuilder(); - } + final PresenceBuilder joinPresenceBuilder = builder.joinPresenceBuilder.ofType(Presence.Type.available); + // Indicate the client supports MUC joinPresenceBuilder.addExtension(new MUCInitialPresence(password, maxChars, maxStanzas, seconds, since)); @@ -95,9 +90,6 @@ public final class MucEnterConfiguration { private final PresenceBuilder joinPresenceBuilder; - // TODO: Remove in Smack 4.5. - private Presence joinPresence; - Builder(Resourcepart nickname, XMPPConnection connection) { this.nickname = Objects.requireNonNull(nickname, "Nickname must not be null"); @@ -109,31 +101,7 @@ public final class MucEnterConfiguration { /** * Set the presence used to join the MUC room. - *

    - * The 'to' value of the given presence will be overridden and the given presence must be of type - * 'available', otherwise an {@link IllegalArgumentException} will be thrown. - *

    - * - * @param presence TODO javadoc me please - * @return a reference to this builder. - * @deprecated use {@link #withPresence(Consumer)} instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public Builder withPresence(Presence presence) { - if (presence.getType() != Presence.Type.available) { - throw new IllegalArgumentException("Presence must be of type 'available'"); - } - - joinPresence = presence; - return this; - } - - /** - * Set the presence used to join the MUC room. - *

    * The consumer must not modify the presence type, otherwise an {@link IllegalArgumentException} will be thrown. - *

    * * @param presenceBuilderConsumer a consumer which will be passed the presence build. * @return a reference to this builder. diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChat.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChat.java index b7fd3ad97..e71f6b523 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChat.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChat.java @@ -1,6 +1,6 @@ /** * - * Copyright 2003-2007 Jive Software. 2020-2022 Florian Schmaus + * Copyright 2003-2007 Jive Software. 2020-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. @@ -87,10 +87,10 @@ import org.jivesoftware.smackx.xdata.form.FillableForm; import org.jivesoftware.smackx.xdata.form.Form; import org.jivesoftware.smackx.xdata.packet.DataForm; +import org.jxmpp.jid.BareJid; import org.jxmpp.jid.DomainBareJid; import org.jxmpp.jid.EntityBareJid; import org.jxmpp.jid.EntityFullJid; -import org.jxmpp.jid.EntityJid; import org.jxmpp.jid.Jid; import org.jxmpp.jid.impl.JidCreate; import org.jxmpp.jid.parts.Resourcepart; @@ -258,13 +258,14 @@ public class MultiUserChat { break; case unavailable: occupantsMap.remove(from); - if (mucUser != null && mucUser.hasStatus()) { - if (isUserStatusModification) { + Set status = mucUser.getStatus(); + if (mucUser != null && !status.isEmpty()) { + if (isUserStatusModification && !status.contains(MUCUser.Status.NEW_NICKNAME_303)) { userHasLeft(); } // Fire events according to the received presence code checkPresenceCode( - mucUser.getStatus(), + status, isUserStatusModification, mucUser, from); @@ -289,8 +290,9 @@ public class MultiUserChat { } for (UserStatusListener listener : userStatusListeners) { - listener.roomDestroyed(alternateMuc, destroy.getReason()); + listener.roomDestroyed(alternateMuc, destroy.getPassword(), destroy.getReason()); } + userHasLeft(); } if (isUserStatusModification) { @@ -350,7 +352,7 @@ public class MultiUserChat { * @throws NoResponseException if there was no response from the remote entity. * @throws XMPPErrorException if there was an XMPP error returned. * @throws InterruptedException if the calling thread was interrupted. - * @throws NotAMucServiceException if the entity is not a MUC serivce. + * @throws NotAMucServiceException if the entity is not a MUC service. * @see XEP-45 7.2 Entering a Room */ private Presence enter(MucEnterConfiguration conf) throws NotConnectedException, NoResponseException, @@ -364,7 +366,7 @@ public class MultiUserChat { // field is in the form "roomName@service/nickname" Presence joinPresence = conf.getJoinPresence(this); - // Setup the messageListeners and presenceListeners *before* the join presence is send. + // Set up the messageListeners and presenceListeners *before* the join presence is sent. connection.addStanzaListener(messageListener, fromRoomGroupchatFilter); StanzaFilter presenceFromRoomFilter = new AndFilter(fromRoomFilter, StanzaTypeFilter.PRESENCE, @@ -400,12 +402,12 @@ public class MultiUserChat { StanzaCollector presenceStanzaCollector = null; final Presence reflectedSelfPresence; try { - // This stanza collector will collect the final self presence from the MUC, which also signals that we have successful entered the MUC. + // This stanza collector will collect the final self presence from the MUC, which also signals that we have successfully entered the MUC. StanzaCollector selfPresenceCollector = connection.createStanzaCollectorAndSend(responseFilter, joinPresence); - StanzaCollector.Configuration presenceStanzaCollectorConfguration = StanzaCollector.newConfiguration().setCollectorToReset( + StanzaCollector.Configuration presenceStanzaCollectorConfiguration = StanzaCollector.newConfiguration().setCollectorToReset( selfPresenceCollector).setStanzaFilter(presenceFromRoomFilter); // This stanza collector is used to reset the timeout of the selfPresenceCollector. - presenceStanzaCollector = connection.createStanzaCollector(presenceStanzaCollectorConfguration); + presenceStanzaCollector = connection.createStanzaCollector(presenceStanzaCollectorConfiguration); reflectedSelfPresence = selfPresenceCollector.nextResultOrThrow(conf.getTimeout()); } catch (NotConnectedException | InterruptedException | NoResponseException | XMPPErrorException e) { @@ -422,15 +424,15 @@ public class MultiUserChat { synchronized (presenceListener) { // Only continue after we have received *and* processed the reflected self-presence. Since presences are // handled in an extra listener, we may return from enter() without having processed all presences of the - // participants, resulting in a e.g. to low participant counter after enter(). Hence we wait here until the + // participants, resulting in a e.g. to low participant counter after enter(). Hence, we wait here until the // processing is done. while (!processedReflectedSelfPresence) { presenceListener.wait(); } } - // This presence must be send from a full JID. We use the resourcepart of this JID as nick, since the room may - // performed roomnick rewriting + // This presence must be sent from a full JID. We use the resourcepart of this JID as nick, since the room may + // have performed roomnick rewriting Resourcepart receivedNickname = reflectedSelfPresence.getFrom().getResourceOrThrow(); setNickname(receivedNickname); @@ -480,7 +482,7 @@ public class MultiUserChat { * @throws NotConnectedException if the XMPP connection is not connected. * @throws MucAlreadyJoinedException if already joined the Multi-User Chat.7y * @throws MissingMucCreationAcknowledgeException if there MUC creation was not acknowledged by the service. - * @throws NotAMucServiceException if the entity is not a MUC serivce. + * @throws NotAMucServiceException if the entity is not a MUC service. */ public synchronized MucCreateConfigFormHandle create(Resourcepart nickname) throws NoResponseException, XMPPErrorException, InterruptedException, MucAlreadyJoinedException, @@ -514,7 +516,7 @@ public class MultiUserChat { * @throws InterruptedException if the calling thread was interrupted. * @throws NotConnectedException if the XMPP connection is not connected. * @throws MucAlreadyJoinedException if already joined the Multi-User Chat.7y - * @throws NotAMucServiceException if the entity is not a MUC serivce. + * @throws NotAMucServiceException if the entity is not a MUC service. */ public synchronized MucCreateConfigFormHandle createOrJoin(Resourcepart nickname) throws NoResponseException, XMPPErrorException, InterruptedException, MucAlreadyJoinedException, NotConnectedException, NotAMucServiceException { @@ -536,7 +538,7 @@ public class MultiUserChat { * @throws InterruptedException if the calling thread was interrupted. * @throws MucAlreadyJoinedException if the MUC is already joined * @throws NotConnectedException if the XMPP connection is not connected. - * @throws NotAMucServiceException if the entity is not a MUC serivce. + * @throws NotAMucServiceException if the entity is not a MUC service. */ public synchronized MucCreateConfigFormHandle createOrJoin(MucEnterConfiguration mucEnterConfiguration) throws NoResponseException, XMPPErrorException, InterruptedException, MucAlreadyJoinedException, NotConnectedException, NotAMucServiceException { @@ -609,7 +611,7 @@ public class MultiUserChat { * @throws XMPPErrorException if there was an XMPP error returned. * @throws NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. - * @throws NotAMucServiceException if the entity is not a MUC serivce. + * @throws NotAMucServiceException if the entity is not a MUC service. */ public MucCreateConfigFormHandle createOrJoinIfNecessary(Resourcepart nickname, String password) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotAMucServiceException { @@ -645,7 +647,7 @@ public class MultiUserChat { * @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. - * @throws NotAMucServiceException if the entity is not a MUC serivce. + * @throws NotAMucServiceException if the entity is not a MUC service. */ public Presence join(Resourcepart nickname) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotAMucServiceException { @@ -675,7 +677,7 @@ public class MultiUserChat { * @throws InterruptedException if the calling thread was interrupted. * @throws NotConnectedException if the XMPP connection is not connected. * @throws NoResponseException if there was no response from the server. - * @throws NotAMucServiceException if the entity is not a MUC serivce. + * @throws NotAMucServiceException if the entity is not a MUC service. */ public void join(Resourcepart nickname, String password) throws XMPPErrorException, InterruptedException, NoResponseException, NotConnectedException, NotAMucServiceException { MucEnterConfiguration.Builder builder = getEnterConfigurationBuilder(nickname).withPassword( @@ -708,7 +710,7 @@ public class MultiUserChat { * @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. - * @throws NotAMucServiceException if the entity is not a MUC serivce. + * @throws NotAMucServiceException if the entity is not a MUC service. */ public synchronized Presence join(MucEnterConfiguration mucEnterConfiguration) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException, NotAMucServiceException { @@ -716,7 +718,7 @@ public class MultiUserChat { // nickname. if (isJoined()) { try { - leaveSync(); + leave(); } catch (XMPPErrorException | NoResponseException | MucNotJoinedException e) { LOGGER.log(Level.WARNING, "Could not leave MUC prior joining, assuming we are not joined", e); @@ -736,23 +738,6 @@ public class MultiUserChat { return getMyRoomJid() != null; } - /** - * Leave the chat room. - * - * @return the leave presence as reflected by the MUC. - * @throws NotConnectedException if the XMPP connection is not connected. - * @throws InterruptedException if the calling thread was interrupted. - * @throws XMPPErrorException if there was an XMPP error returned. - * @throws NoResponseException if there was no response from the remote entity. - * @throws MucNotJoinedException if not joined to the Multi-User Chat. - * @deprecated use {@link #leave()} instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public synchronized Presence leaveSync() throws NotConnectedException, InterruptedException, MucNotJoinedException, NoResponseException, XMPPErrorException { - return leave(); - } - /** * Leave the chat room. * @@ -832,9 +817,9 @@ public class MultiUserChat { /** * Returns the room's configuration form that the room's owner can use. * The configuration form allows to set the room's language, - * enable logging, specify room's type, etc.. + * enable logging, specify room's type, etc. * - * @return the Form that contains the fields to complete together with the instrucions or + * @return the Form that contains the fields to complete together with the instructions or * null if no configuration is possible. * @throws XMPPErrorException if an error occurs asking the configuration form for the room. * @throws NoResponseException if there was no response from the server. @@ -888,7 +873,7 @@ public class MultiUserChat { * error to the user (error code 405). * * @return the registration Form that contains the fields to complete together with the - * instrucions or null if no registration is possible. + * instructions or null if no registration is possible. * @throws XMPPErrorException if an error occurs asking the registration form for the room or a * 405 error if the user is not allowed to register with the room. * @throws NoResponseException if there was no response from the server. @@ -964,12 +949,32 @@ public class MultiUserChat { * @throws InterruptedException if the calling thread was interrupted. */ public void destroy(String reason, EntityBareJid alternateJID) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { + destroy(reason, alternateJID, null); + } + + /** + * Sends a request to the server to destroy the room. The sender of the request + * should be the room's owner. If the sender of the destroy request is not the room's owner + * then the server will answer a "Forbidden" error (403). + * + * @param reason an optional reason for the room destruction. + * @param alternateJID an optional JID of an alternate location. + * @param password an optional password for the alternate location + * @throws XMPPErrorException if an error occurs while trying to destroy the room. + * An error can occur which will be wrapped by an XMPPException -- + * XMPP error code 403. The error code can be used to present more + * appropriate error messages to end-users. + * @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 destroy(String reason, EntityBareJid alternateJID, String password) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { MUCOwner iq = new MUCOwner(); iq.setTo(room); iq.setType(IQ.Type.set); // Create the reason for the room destruction - Destroy destroy = new Destroy(alternateJID, reason); + Destroy destroy = new Destroy(alternateJID, password, reason); iq.setDestroy(destroy); try { @@ -1006,36 +1011,6 @@ public class MultiUserChat { invite(connection.getStanzaFactory().buildMessageStanza(), user, reason); } - /** - * Invites another user to the room in which one is an occupant using a given Message. The invitation - * will be sent to the room which in turn will forward the invitation to the invitee.

    - * - * If the room is password-protected, the invitee will receive a password to use to join - * the room. If the room is members-only, the invitee may be added to the member list. - * - * @param message the message to use for sending the invitation. - * @param user the user to invite to the room.(e.g. hecate@shakespeare.lit) - * @param reason the reason why the user is being invited. - * @throws NotConnectedException if the XMPP connection is not connected. - * @throws InterruptedException if the calling thread was interrupted. - * @deprecated use {@link #invite(MessageBuilder, EntityBareJid, String)} instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public void invite(Message message, EntityBareJid user, String reason) throws NotConnectedException, InterruptedException { - // TODO listen for 404 error code when inviter supplies a non-existent JID - message.setTo(room); - - // Create the MUCUser packet that will include the invitation - MUCUser mucUser = new MUCUser(); - MUCUser.Invite invite = new MUCUser.Invite(reason, user); - mucUser.setInvite(invite); - // Add the MUCUser packet that includes the invitation to the message - message.addExtension(mucUser); - - connection.sendStanza(message); - } - /** * Invites another user to the room in which one is an occupant using a given Message. The invitation * will be sent to the room which in turn will forward the invitation to the invitee.

    @@ -1273,6 +1248,8 @@ public class MultiUserChat { return myRoomJid; } + private static final Object changeNicknameLock = new Object(); + /** * Changes the occupant's nickname to a new nickname within the room. Each room occupant * will receive two presence packets. One of type "unavailable" for the old nickname and one @@ -1287,7 +1264,7 @@ public class MultiUserChat { * @throws InterruptedException if the calling thread was interrupted. * @throws MucNotJoinedException if not joined to the Multi-User Chat. */ - public synchronized void changeNickname(Resourcepart nickname) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, MucNotJoinedException { + public void changeNickname(Resourcepart nickname) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, MucNotJoinedException { Objects.requireNonNull(nickname, "Nickname must not be null or blank."); // Check that we already have joined the room before attempting to change the // nickname. @@ -1303,18 +1280,20 @@ public class MultiUserChat { .ofType(Presence.Type.available) .build(); - // Wait for a presence packet back from the server. - StanzaFilter responseFilter = - new AndFilter( - FromMatchesFilter.createFull(jid), - new StanzaTypeFilter(Presence.class)); - StanzaCollector response = connection.createStanzaCollectorAndSend(responseFilter, joinPresence); - // Wait up to a certain number of seconds for a reply. If there is a negative reply, an - // exception will be thrown - response.nextResultOrThrow(); + synchronized (changeNicknameLock) { + // Wait for a presence packet back from the server. + StanzaFilter responseFilter = + new AndFilter( + FromMatchesFilter.createFull(jid), + new StanzaTypeFilter(Presence.class)); + StanzaCollector response = connection.createStanzaCollectorAndSend(responseFilter, joinPresence); + // Wait up to a certain number of seconds for a reply. If there is a negative reply, an + // exception will be thrown + response.nextResultOrThrow(); - // TODO: Shouldn't this handle nickname rewriting by the MUC service? - setNickname(nickname); + // TODO: Shouldn't this handle nickname rewriting by the MUC service? + setNickname(nickname); + } } /** @@ -1484,7 +1463,7 @@ public class MultiUserChat { * @throws NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. */ - public void banUsers(Collection jids) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException { + public void banUsers(Collection jids) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException { changeAffiliationByAdmin(jids, MUCAffiliation.outcast); } @@ -1504,7 +1483,7 @@ public class MultiUserChat { * @throws NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. */ - public void banUser(Jid jid, String reason) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException { + public void banUser(BareJid jid, String reason) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException { changeAffiliationByAdmin(jid, MUCAffiliation.outcast, reason); } @@ -1534,7 +1513,7 @@ public class MultiUserChat { * @throws NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. */ - public void grantMembership(Jid jid) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException { + public void grantMembership(BareJid jid) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException { changeAffiliationByAdmin(jid, MUCAffiliation.member, null); } @@ -1550,7 +1529,7 @@ public class MultiUserChat { * @throws NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. */ - public void revokeMembership(Collection jids) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException { + public void revokeMembership(Collection jids) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException { changeAffiliationByAdmin(jids, MUCAffiliation.none); } @@ -1566,14 +1545,14 @@ public class MultiUserChat { * @throws NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. */ - public void revokeMembership(Jid jid) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException { + public void revokeMembership(BareJid jid) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException { changeAffiliationByAdmin(jid, MUCAffiliation.none, null); } /** * Grants moderator privileges to participants or visitors. Room administrators may grant * moderator privileges. A moderator is allowed to kick users, grant and revoke voice, invite - * other users, modify room's subject plus all the partcipants privileges. + * other users, modify room's subject plus all the participant privileges. * * @param nicknames the nicknames of the occupants to grant moderator privileges. * @throws XMPPErrorException if an error occurs granting moderator privileges to a user. @@ -1588,7 +1567,7 @@ public class MultiUserChat { /** * Grants moderator privileges to a participant or visitor. Room administrators may grant * moderator privileges. A moderator is allowed to kick users, grant and revoke voice, invite - * other users, modify room's subject plus all the partcipants privileges. + * other users, modify room's subject plus all the participant privileges. * * @param nickname the nickname of the occupant to grant moderator privileges. * @throws XMPPErrorException if an error occurs granting moderator privileges to a user. @@ -1644,7 +1623,7 @@ public class MultiUserChat { * @throws NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. */ - public void grantOwnership(Collection jids) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException { + public void grantOwnership(Collection jids) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException { changeAffiliationByAdmin(jids, MUCAffiliation.owner); } @@ -1660,7 +1639,7 @@ public class MultiUserChat { * @throws NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. */ - public void grantOwnership(Jid jid) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException { + public void grantOwnership(BareJid jid) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException { changeAffiliationByAdmin(jid, MUCAffiliation.owner, null); } @@ -1675,7 +1654,7 @@ public class MultiUserChat { * @throws NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. */ - public void revokeOwnership(Collection jids) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException { + public void revokeOwnership(Collection jids) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException { changeAffiliationByAdmin(jids, MUCAffiliation.admin); } @@ -1690,7 +1669,7 @@ public class MultiUserChat { * @throws NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. */ - public void revokeOwnership(Jid jid) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException { + public void revokeOwnership(BareJid jid) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException { changeAffiliationByAdmin(jid, MUCAffiliation.admin, null); } @@ -1705,7 +1684,7 @@ public class MultiUserChat { * @throws NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. */ - public void grantAdmin(Collection jids) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException { + public void grantAdmin(Collection jids) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException { changeAffiliationByAdmin(jids, MUCAffiliation.admin); } @@ -1721,7 +1700,7 @@ public class MultiUserChat { * @throws NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. */ - public void grantAdmin(Jid jid) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException { + public void grantAdmin(BareJid jid) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException { changeAffiliationByAdmin(jid, MUCAffiliation.admin); } @@ -1736,7 +1715,7 @@ public class MultiUserChat { * @throws NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. */ - public void revokeAdmin(Collection jids) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException { + public void revokeAdmin(Collection jids) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException { changeAffiliationByAdmin(jids, MUCAffiliation.admin); } @@ -1752,7 +1731,7 @@ public class MultiUserChat { * @throws NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. */ - public void revokeAdmin(EntityJid jid) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException { + public void revokeAdmin(BareJid jid) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException { changeAffiliationByAdmin(jid, MUCAffiliation.member); } @@ -1863,7 +1842,7 @@ public class MultiUserChat { /** * Returns the presence info for a particular user, or null if the user - * is not in the room.

    + * is not in the room. * * @param user the room occupant to search for his presence. The format of user must * be: roomName@service/nickname (e.g. darkcave@macbeth.shakespeare.lit/thirdwitch). @@ -1877,7 +1856,7 @@ public class MultiUserChat { /** * Returns the Occupant information for a particular occupant, or null if the * user is not in the room. The Occupant object may include information such as full - * JID of the user as well as the role and affiliation of the user in the room.

    + * JID of the user as well as the role and affiliation of the user in the room. * * @param user the room occupant to search for his presence. The format of user must * be: roomName@service/nickname (e.g. darkcave@macbeth.shakespeare.lit/thirdwitch). @@ -2111,20 +2090,6 @@ public class MultiUserChat { ; } - /** - * Sends a Message to the chat room. - * - * @param message the message. - * @throws NotConnectedException if the XMPP connection is not connected. - * @throws InterruptedException if the calling thread was interrupted. - * @deprecated use {@link #sendMessage(MessageBuilder)} instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public void sendMessage(Message message) throws NotConnectedException, InterruptedException { - sendMessage(message.asBuilder()); - } - /** * Sends a Message to the chat room. * @@ -2146,7 +2111,7 @@ public class MultiUserChat { /** * Polls for and returns the next message, or null if there isn't * a message immediately available. This method provides significantly different - * functionalty than the {@link #nextMessage()} method since it's non-blocking. + * functionality than the {@link #nextMessage()} method since it's non-blocking. * In other words, the method call will always return immediately, whereas the * nextMessage method will return only when a message is available (or after * a specific timeout). @@ -2179,7 +2144,7 @@ public class MultiUserChat { /** * Returns the next available message in the chat. The method call will block - * (not return) until a stanza is available or the timeout has elapased. + * (not return) until a stanza is available or the timeout has elapsed. * If the timeout elapses without a result, null will be returned. * * @param timeout the maximum amount of time to wait for the next message. @@ -2243,17 +2208,20 @@ public class MultiUserChat { * @throws InterruptedException if the calling thread was interrupted. */ public void changeSubject(final String subject) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { - MessageBuilder message = buildMessage(); - message.setSubject(subject); + Message message = buildMessage() + .setSubject(subject) + .build(); // Wait for an error or confirmation message back from the server. - StanzaFilter responseFilter = new AndFilter(fromRoomGroupchatFilter, new StanzaFilter() { + StanzaFilter successFilter = new AndFilter(fromRoomGroupchatFilter, new StanzaFilter() { @Override public boolean accept(Stanza packet) { Message msg = (Message) packet; return subject.equals(msg.getSubject()); } }); - StanzaCollector response = connection.createStanzaCollectorAndSend(responseFilter, message.build()); + StanzaFilter errorFilter = new AndFilter(fromRoomFilter, new StanzaIdFilter(message), MessageTypeFilter.ERROR); + StanzaFilter responseFilter = new OrFilter(successFilter, errorFilter); + StanzaCollector response = connection.createStanzaCollectorAndSend(responseFilter, message); // Wait up to a certain number of seconds for a reply. response.nextResultOrThrow(); } @@ -2628,6 +2596,10 @@ public class MultiUserChat { for (UserStatusListener listener : userStatusListeners) { listener.membershipRevoked(); } + } else { + for (ParticipantStatusListener listener : participantStatusListeners) { + listener.membershipRevoked(from); + } } } // A occupant has changed his nickname in the room diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatException.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatException.java index 1f7f930a2..11de8dff4 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatException.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatException.java @@ -1,6 +1,6 @@ /** * - * Copyright © 2014-2015 Florian Schmaus + * Copyright © 2014-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. @@ -88,7 +88,7 @@ public abstract class MultiUserChatException extends SmackException { /** * Thrown when trying to enter a MUC room that is not hosted a domain providing a MUC service. - * Try {@link MultiUserChatManager#getXMPPServiceDomains()} for a list of client-local domains + * Try {@link MultiUserChatManager#getMucServiceDomains()} for a list of client-local domains * providing a MUC service. */ public static class NotAMucServiceException extends MultiUserChatException { 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..4d8c5c637 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 @@ -1,6 +1,6 @@ /** * - * Copyright © 2014-2021 Florian Schmaus + * Copyright © 2014-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. @@ -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 { @@ -104,6 +108,7 @@ public final class MultiUserChatManager extends Manager { final WeakReference weakRefConnection = new WeakReference(connection); ServiceDiscoveryManager.getInstanceFor(connection).setNodeInformationProvider(DISCO_NODE, new AbstractNodeInformationProvider() { + @SuppressWarnings({"JavaUtilDate", "MixedMutabilityReturnType"}) @Override public List getNodeItems() { XMPPConnection connection = weakRefConnection.get(); @@ -144,6 +149,7 @@ public final class MultiUserChatManager extends Manager { private static final StanzaFilter DIRECT_INVITATION_FILTER = new AndFilter(StanzaTypeFilter.MESSAGE, new ExtensionElementFilter(GroupChatInvitation.class), + NotFilter.of(MUCUser.class), new NotFilter(MessageTypeFilter.ERROR)); private static final ExpirationCache KNOWN_MUC_SERVICES = new ExpirationCache<>( @@ -293,7 +299,7 @@ public final class MultiUserChatManager extends Manager { * {@link MultiUserChat#join(org.jxmpp.jid.parts.Resourcepart) join} the chat room. On some server implementations, the room will not be * created until the first person joins it. *

    - * Most XMPP servers use a sub-domain for the chat service (eg chat.example.com for the XMPP server example.com). + * Most XMPP servers use a sub-domain for the chat service (e.g.chat.example.com for the XMPP server example.com). * You must ensure that the room address you're trying to connect to includes the proper chat sub-domain. *

    * @@ -416,22 +422,6 @@ public final class MultiUserChatManager extends Manager { return serviceDiscoveryManager.findServices(MUCInitialPresence.NAMESPACE, false, false); } - /** - * Returns a collection with the XMPP addresses of the Multi-User Chat services. - * - * @return a collection with the XMPP addresses of the Multi-User Chat services. - * @throws XMPPErrorException if there was an XMPP error returned. - * @throws NoResponseException if there was no response from the remote entity. - * @throws NotConnectedException if the XMPP connection is not connected. - * @throws InterruptedException if the calling thread was interrupted. - * @deprecated use {@link #getMucServiceDomains()} instead. - */ - // TODO: Remove in Smack 4.5 - @Deprecated - public List getXMPPServiceDomains() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { - return getMucServiceDomains(); - } - /** * Check if the provided domain bare JID provides a MUC service. * @@ -476,7 +466,7 @@ public final class MultiUserChatManager extends Manager { * @throws NoResponseException if there was no response from the remote entity. * @throws NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. - * @throws NotAMucServiceException if the entity is not a MUC serivce. + * @throws NotAMucServiceException if the entity is not a MUC service. * @since 4.3.1 */ public Map getRoomsHostedBy(DomainBareJid serviceName) throws NoResponseException, XMPPErrorException, diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/ParticipantStatusListener.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/ParticipantStatusListener.java index 8a376ca36..58454d6e9 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/ParticipantStatusListener.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/ParticipantStatusListener.java @@ -129,7 +129,7 @@ public interface ParticipantStatusListener { /** * Called when an administrator grants moderator privileges to a user. This means that the user * will be able to kick users, grant and revoke voice, invite other users, modify room's - * subject plus all the partcipants privileges. + * subject plus all the participant privileges. * * @param participant the participant that was granted moderator privileges in the room * (e.g. room@conference.jabber.org/nick). @@ -140,7 +140,7 @@ public interface ParticipantStatusListener { /** * Called when an administrator revokes moderator privileges from a user. This means that the * user will no longer be able to kick users, grant and revoke voice, invite other users, - * modify room's subject plus all the partcipants privileges. + * modify room's subject plus all the participant privileges. * * @param participant the participant that was revoked moderator privileges in the room * (e.g. room@conference.jabber.org/nick). diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/RoomInfo.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/RoomInfo.java index d321903dd..b43e33ae9 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/RoomInfo.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/RoomInfo.java @@ -77,7 +77,7 @@ public class RoomInfo { */ private final boolean moderated; /** - * Every presence stanza can include the JID of every occupant unless the owner deactives this + * Every presence stanza can include the JID of every occupant unless the owner deactivates this * configuration. */ private final boolean nonanonymous; @@ -250,7 +250,7 @@ public class RoomInfo { /** * Returns the room name. *

    - * The name returnd here was provided as value of the name attribute + * The name returned here was provided as value of the name attribute * of the returned identity within the disco#info result. *

    * @@ -324,9 +324,9 @@ public class RoomInfo { } /** - * Returns true if users musy provide a valid password in order to join the room. + * Returns true if users must provide a valid password in order to join the room. * - * @return true if users musy provide a valid password in order to join the room. + * @return true if users must provide a valid password in order to join the room. */ public boolean isPasswordProtected() { return passwordProtected; diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/UserStatusListener.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/UserStatusListener.java index 0e87cb5bd..93cf585fe 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/UserStatusListener.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/UserStatusListener.java @@ -28,7 +28,7 @@ import org.jxmpp.jid.Jid; * banned, or granted admin permissions or the room is destroyed. *

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

    @@ -39,7 +39,7 @@ public interface UserStatusListener { /** * Called when a moderator kicked your user from the room. This means that you are no longer - * participanting in the room. + * participating in the room. * * @param actor the moderator that kicked your user from the room (e.g. user@host.org). * @param reason the reason provided by the actor to kick you from the room. @@ -106,7 +106,7 @@ public interface UserStatusListener { /** * Called when an administrator grants moderator privileges to your user. This means that you * will be able to kick users, grant and revoke voice, invite other users, modify room's - * subject plus all the partcipants privileges. + * subject plus all the participant privileges. * */ default void moderatorGranted() { @@ -115,7 +115,7 @@ public interface UserStatusListener { /** * Called when an administrator revokes moderator privileges from your user. This means that * you will no longer be able to kick users, grant and revoke voice, invite other users, - * modify room's subject plus all the partcipants privileges. + * modify room's subject plus all the participant privileges. * */ default void moderatorRevoked() { @@ -161,10 +161,11 @@ public interface UserStatusListener { * Called when the room is destroyed. * * @param alternateMUC an alternate MultiUserChat, may be null. + * @param password a password for the alternative MultiUserChat, may be null. * @param reason the reason why the room was closed, may be null. * @see #removed(MUCUser, Presence) */ - default void roomDestroyed(MultiUserChat alternateMUC, String reason) { + default void roomDestroyed(MultiUserChat alternateMUC, String password, String reason) { } } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/package-info.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/package-info.java index 5adaa5a6f..78866dd6a 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/package-info.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/package-info.java @@ -534,7 +534,7 @@ *

    *

    Usage

    *

    - * In order to grant membership to a room, administrator privileges or owner priveliges just send + * In order to grant membership to a room, administrator privileges or owner privileges just send * **grantMembership(String jid)**, **grantAdmin(String jid)** or **grantOwnership(String jid)** to _**MultiUserChat**_ * respectively. Use **revokeMembership(String jid)**, **revokeAdmin(String jid)** or revokeOwnership(String jid)** to * revoke the membership to a room, administrator privileges or owner priveliges respectively. diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/packet/Destroy.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/packet/Destroy.java index 1361f79fe..9da151c4c 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/packet/Destroy.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/packet/Destroy.java @@ -40,14 +40,22 @@ public class Destroy implements NamedElement, Serializable { private final String reason; private final EntityBareJid jid; + private final String password; public Destroy(Destroy other) { - this(other.jid, other.reason); + this(other.jid, other.password, other.reason); } public Destroy(EntityBareJid alternativeJid, String reason) { this.jid = alternativeJid; this.reason = reason; + this.password = null; + } + + public Destroy(EntityBareJid alternativeJid, String password, String reason) { + this.jid = alternativeJid; + this.password = password; + this.reason = reason; } /** @@ -59,6 +67,15 @@ public class Destroy implements NamedElement, Serializable { return jid; } + /** + * Returns the password of the alternate location. + * + * @return the password of the alternate location. + */ + public String getPassword() { + return password; + } + /** * Returns the reason for the room destruction. * @@ -73,6 +90,7 @@ public class Destroy implements NamedElement, Serializable { XmlStringBuilder xml = new XmlStringBuilder(this); xml.optAttribute("jid", getJid()); xml.rightAngleBracket(); + xml.optElement("password", getPassword()); xml.optElement("reason", getReason()); xml.closeElement(this); return xml; 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/packet/MUCItem.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/packet/MUCItem.java index c1ee5ae28..8da0efb78 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/packet/MUCItem.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/packet/MUCItem.java @@ -169,8 +169,11 @@ public class MUCItem implements NamedElement { xml.optAttribute("role", getRole()); xml.rightAngleBracket(); xml.optElement("reason", getReason()); - if (getActor() != null) { - xml.halfOpenElement("actor").attribute("jid", getActor()).closeEmptyElement(); + if (getActor() != null || getActorNick() != null) { + xml.halfOpenElement("actor"); + xml.optAttribute("jid", getActor()); + xml.optAttribute("nick", getActorNick()); + xml.closeEmptyElement(); } xml.closeElement(Stanza.ITEM); return xml; 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 * In order to handle privacy changes, clients SHOULD listen manager’s updates. When a list is changed the manager * notifies every added listener. Listeners MUST implement the PrivacyListListener interface. Clients may - * need to react when a privacy list is modified. The PrivacyListManager lets you add listerners that will + * need to react when a privacy list is modified. The PrivacyListManager lets you add listeners that will * be notified when a list has been changed. Listeners should implement the PrivacyListListener interface. *

    *

    diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/privacy/packet/PrivacyItem.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/privacy/packet/PrivacyItem.java index c977fdaf3..a80276e5b 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/privacy/packet/PrivacyItem.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/privacy/packet/PrivacyItem.java @@ -171,7 +171,7 @@ public class PrivacyItem { } /** - * Sets wheather the receiver allows or denies incoming messages or not. + * Sets whether the receiver allows or denies incoming messages or not. * * @param filterMessage indicates if the receiver allows or denies incoming messages or not. */ diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/ItemDeleteEvent.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/ItemDeleteEvent.java index 424c3da0b..3ac132246 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/ItemDeleteEvent.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/ItemDeleteEvent.java @@ -31,7 +31,7 @@ public class ItemDeleteEvent extends SubscriptionEvent { * Constructs an ItemDeleteEvent that indicates the supplied * items (by id) have been deleted, and that the event matches the listed * subscriptions. The subscriptions would have been created by calling - * {@link LeafNode#subscribe(String)}. + * {@link LeafNode#subscribe(org.jxmpp.Jid)}. * * @param nodeId The id of the node the event came from * @param deletedItemIds The item ids of the items that were deleted. diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/ItemPublishEvent.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/ItemPublishEvent.java index 577dad4f7..7018b5934 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/ItemPublishEvent.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/ItemPublishEvent.java @@ -109,6 +109,7 @@ public class ItemPublishEvent extends SubscriptionEvent { return originalDate; } + @SuppressWarnings("JavaUtilDate") @Override public String toString() { return getClass().getName() + " [subscriptions: " + getSubscriptions() + "], [Delayed: " + diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/Node.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/Node.java index 4db909dfa..9741ce51f 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/Node.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/Node.java @@ -52,8 +52,6 @@ import org.jivesoftware.smackx.shim.packet.HeadersExtension; import org.jivesoftware.smackx.xdata.packet.DataForm; import org.jxmpp.jid.Jid; -import org.jxmpp.jid.impl.JidCreate; -import org.jxmpp.stringprep.XmppStringprepException; public abstract class Node { protected final PubSubManager pubSubManager; @@ -403,39 +401,6 @@ public abstract class Node { return reply.getExtension(PubSubElementType.SUBSCRIPTION); } - /** - * The user subscribes to the node using the supplied jid. The - * bare jid portion of this one must match the jid for the connection. - * - * Please note that the {@link Subscription.State} should be checked - * on return since more actions may be required by the caller. - * {@link Subscription.State#pending} - The owner must approve the subscription - * request before messages will be received. - * {@link Subscription.State#unconfigured} - If the {@link Subscription#isConfigRequired()} is true, - * the caller must configure the subscription before messages will be received. If it is false - * the caller can configure it but is not required to do so. - * - * @param jidString The jid to subscribe as. - * @return The subscription - * @throws XMPPErrorException if there was an XMPP error returned. - * @throws NoResponseException if there was no response from the remote entity. - * @throws NotConnectedException if the XMPP connection is not connected. - * @throws InterruptedException if the calling thread was interrupted. - * @throws IllegalArgumentException if the provided string is not a valid JID. - * @deprecated use {@link #subscribe(Jid)} instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public Subscription subscribe(String jidString) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { - Jid jid; - try { - jid = JidCreate.from(jidString); - } catch (XmppStringprepException e) { - throw new IllegalArgumentException(e); - } - return subscribe(jid); - } - /** * The user subscribes to the node using the supplied jid and subscription * options. The bare jid portion of this one must match the jid for the @@ -466,42 +431,6 @@ public abstract class Node { return reply.getExtension(PubSubElementType.SUBSCRIPTION); } - /** - * The user subscribes to the node using the supplied jid and subscription - * options. The bare jid portion of this one must match the jid for the - * connection. - * - * Please note that the {@link Subscription.State} should be checked - * on return since more actions may be required by the caller. - * {@link Subscription.State#pending} - The owner must approve the subscription - * request before messages will be received. - * {@link Subscription.State#unconfigured} - If the {@link Subscription#isConfigRequired()} is true, - * the caller must configure the subscription before messages will be received. If it is false - * the caller can configure it but is not required to do so. - * - * @param jidString The jid to subscribe as. - * @param subForm TODO javadoc me please - * - * @return The subscription - * @throws XMPPErrorException if there was an XMPP error returned. - * @throws NoResponseException if there was no response from the remote entity. - * @throws NotConnectedException if the XMPP connection is not connected. - * @throws InterruptedException if the calling thread was interrupted. - * @throws IllegalArgumentException if the provided string is not a valid JID. - * @deprecated use {@link #subscribe(Jid, FillableSubscribeForm)} instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public Subscription subscribe(String jidString, FillableSubscribeForm subForm) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { - Jid jid; - try { - jid = JidCreate.from(jidString); - } catch (XmppStringprepException e) { - throw new IllegalArgumentException(e); - } - return subscribe(jid, subForm); - } - /** * Remove the subscription related to the specified JID. This will only * work if there is only 1 subscription. If there are multiple subscriptions, diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/PubSubManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/PubSubManager.java index cbe9c10cb..3c7c756de 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/PubSubManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/PubSubManager.java @@ -156,33 +156,6 @@ public final class PubSubManager extends Manager { return pubSubManager; } - /** - * Deprecated. - * - * @param connection the connection. - * @return the PubSub manager for the given connection. - * @deprecated use {@link #getInstanceFor(XMPPConnection)} instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public static PubSubManager getInstance(XMPPConnection connection) { - return getInstanceFor(connection); - } - - /** - * Deprecated. - * - * @param connection the connection. - * @param pubSubService the XMPP address of the PubSub service. - * @return the PubSub manager for the given connection. - * @deprecated use {@link #getInstanceFor(XMPPConnection, BareJid)} instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5. - public static PubSubManager getInstance(XMPPConnection connection, BareJid pubSubService) { - return getInstanceFor(connection, pubSubService); - } - /** * Create a pubsub manager associated to the specified connection where * the pubsub requests require a specific to address for packets. @@ -263,7 +236,7 @@ public final class PubSubManager extends Manager { DataForm submitForm = config.getDataFormToSubmit(); request.addExtension(new FormNode(FormNodeType.CONFIGURE, submitForm)); NodeType nodeType = config.getNodeType(); - // Note that some implementations do to have the pubsub#node_type field in their defauilt configuration, + // Note that some implementations do to have the pubsub#node_type field in their default configuration, // which I believe to be a bug. However, since PubSub specifies the default node type to be 'leaf' we assume // leaf if the field does not exist. isLeafNode = nodeType == null || nodeType == NodeType.leaf; diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/SimplePayload.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/SimplePayload.java index 200be7f4b..95667a6e5 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/SimplePayload.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/SimplePayload.java @@ -59,27 +59,6 @@ public class SimplePayload implements XmlElement { ns = StringUtils.requireNotNullNorEmpty(qname.getNamespaceURI(), "Could not determine namespace from XML payload"); } - /** - * Construct a SimplePayload object with the specified element name, - * namespace and content. The content must be well formed XML. - * - * @param elementName The root element name (of the payload) - * @param namespace The namespace of the payload, null if there is none - * @param xmlPayload The payload data - * @deprecated use {@link #SimplePayload(String)} insteas. - */ - // TODO: Remove in Smack 4.5 - @Deprecated - public SimplePayload(String elementName, String namespace, CharSequence xmlPayload) { - this(xmlPayload.toString()); - if (!elementName.equals(this.elemName)) { - throw new IllegalArgumentException(); - } - if (!namespace.equals(this.ns)) { - throw new IllegalArgumentException(); - } - } - @Override public String getElementName() { return elemName; diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/packet/PubSub.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/packet/PubSub.java index a2756d5ff..138558207 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/packet/PubSub.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/packet/PubSub.java @@ -43,13 +43,14 @@ public class PubSub extends IQ { super(ELEMENT, ns.getXmlns()); } + @SuppressWarnings("this-escape") public PubSub(Jid to, Type type, PubSubNamespace ns) { super(ELEMENT, (ns == null ? PubSubNamespace.basic : ns).getXmlns()); setTo(to); setType(type); } - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) public PE getExtension(PubSubElementType elem) { return (PE) getExtensionElement(elem.getElementName(), elem.getNamespace().getXmlns()); } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/rsm/RSMManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/rsm/RSMManager.java index ea6924fae..9f03c257a 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/rsm/RSMManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/rsm/RSMManager.java @@ -1,6 +1,6 @@ /** * - * Copyright © 2014-2021 Florian Schmaus + * Copyright © 2014-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. @@ -16,8 +16,8 @@ */ package org.jivesoftware.smackx.rsm; +import java.util.ArrayList; import java.util.Collection; -import java.util.LinkedList; import java.util.List; import org.jivesoftware.smack.packet.XmlElement; @@ -29,7 +29,7 @@ import org.jivesoftware.smackx.rsm.packet.RSMSet.PageDirection; public class RSMManager { Collection page(int max) { - List packetExtensions = new LinkedList<>(); + List packetExtensions = new ArrayList<>(); packetExtensions.add(new RSMSet(max)); return packetExtensions; } @@ -45,7 +45,7 @@ public class RSMManager { throw new IllegalArgumentException("returnedExtensions must no be null"); } if (additionalExtensions == null) { - additionalExtensions = new LinkedList<>(); + additionalExtensions = new ArrayList<>(); } RSMSet resultRsmSet = PacketUtil.extensionElementFrom(returnedExtensions, RSMSet.ELEMENT, RSMSet.NAMESPACE); if (resultRsmSet == null) { diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/search/ReportedData.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/search/ReportedData.java index 36853164b..ac2cec39d 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/search/ReportedData.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/search/ReportedData.java @@ -150,7 +150,7 @@ public class ReportedData { /** * Creates a new column with the specified definition. * - * @param label the columns's label. + * @param label the column's label. * @param variable the variable name of the column. * @param type the format for the returned data. */ diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/si/provider/StreamInitiationProvider.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/si/provider/StreamInitiationProvider.java index a4be338b5..d6333ed80 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/si/provider/StreamInitiationProvider.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/si/provider/StreamInitiationProvider.java @@ -45,6 +45,7 @@ import org.jxmpp.util.XmppDateTime; public class StreamInitiationProvider extends IqProvider { private static final Logger LOGGER = Logger.getLogger(StreamInitiationProvider.class.getName()); + @SuppressWarnings("JavaUtilDate") @Override public StreamInitiation parse(XmlPullParser parser, int initialDepth, IqData iqData, XmlEnvironment xmlEnvironment) throws XmlPullParserException, IOException, SmackParsingException { // si diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/time/packet/Time.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/time/packet/Time.java index bb7632210..387a7d390 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/time/packet/Time.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/time/packet/Time.java @@ -1,6 +1,6 @@ /** * - * Copyright 2003-2007 Jive Software, 2014-2021 Florian Schmaus + * Copyright 2003-2007 Jive Software, 2014-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. @@ -46,6 +46,7 @@ public class Time extends IQ implements TimeView { private final String utc; private final String tzo; + @SuppressWarnings("this-escape") public Time(TimeBuilder timeBuilder) { super(timeBuilder, ELEMENT, NAMESPACE); utc = timeBuilder.getUtc(); diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/element/DescElement.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/element/DescElement.java new file mode 100644 index 000000000..7cb6b4b6b --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/element/DescElement.java @@ -0,0 +1,67 @@ +/** + * + * 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.urldata.element; + +import org.jivesoftware.smack.packet.NamedElement; +import org.jivesoftware.smack.packet.XmlEnvironment; +import org.jivesoftware.smack.util.EqualsUtil; +import org.jivesoftware.smack.util.HashCode; +import org.jivesoftware.smack.util.XmlStringBuilder; + +public class DescElement implements NamedElement { + + public static final String ELEMENT = "desc"; + + private final String desc; + + public DescElement(String desc) { + this.desc = desc; + } + + public String getDesc() { + return desc; + } + + @Override + public XmlStringBuilder toXML(XmlEnvironment xmlEnvironment) { + return new XmlStringBuilder(this) + .rightAngleBracket() + .append(getDesc()) + .closeElement(this); + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public int hashCode() { + return HashCode.builder() + .append(getElementName()) + .append(getDesc()) + .build(); + } + + @Override + public boolean equals(Object obj) { + return EqualsUtil.equals(this, obj, (equalsBuilder, other) -> + equalsBuilder + .append(getElementName(), other.getElementName()) + .append(getDesc(), other.getDesc())); + } +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/element/MetaInformationElement.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/element/MetaInformationElement.java new file mode 100644 index 000000000..95628092b --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/element/MetaInformationElement.java @@ -0,0 +1,22 @@ +/** + * + * 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.urldata.element; + +import org.jivesoftware.smack.packet.NamedElement; + +public interface MetaInformationElement extends NamedElement { +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/element/UrlDataElement.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/element/UrlDataElement.java new file mode 100644 index 000000000..b10478e44 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/element/UrlDataElement.java @@ -0,0 +1,160 @@ +/** + * + * 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.urldata.element; + +import java.util.ArrayList; +import java.util.List; + +import org.jivesoftware.smack.packet.ExtensionElement; +import org.jivesoftware.smack.packet.NamedElement; +import org.jivesoftware.smack.packet.XmlEnvironment; +import org.jivesoftware.smack.util.EqualsUtil; +import org.jivesoftware.smack.util.HashCode; +import org.jivesoftware.smack.util.XmlStringBuilder; + +import org.jivesoftware.smackx.urldata.http.element.CookieElement; +import org.jivesoftware.smackx.urldata.http.element.HeaderElement; +import org.jivesoftware.smackx.urldata.http.element.HttpAuthElement; + +/** + * The url-data element. + */ +public class UrlDataElement implements ExtensionElement { + + public static final String ELEMENT = "url-data"; + public static final String NAMESPACE = "http://jabber.org/protocol/url-data"; + public static final String ATTR_TARGET = "target"; + public static final String ATTR_SID = "sid"; + public static final String XMLNS_HTTP = "xmlns:http"; + + public static final String SCHEME_HTTP = "http://jabber.org/protocol/url-data/scheme/http"; + + private final String target; + private final String sid; + private final List authParamElements = new ArrayList<>(); + private final List cookieElements = new ArrayList<>(); + private final List headerElements = new ArrayList<>(); + + public UrlDataElement(String target, + String sid) { + this(target, sid, null, null, null); + } + + public UrlDataElement(String target, + String sid, + List authParamElements, + List cookieElements, + List headerElements) { + this.target = target; + this.sid = sid; + if (authParamElements != null) { + this.authParamElements.addAll(authParamElements); + } + if (cookieElements != null) { + this.cookieElements.addAll(cookieElements); + } + if (headerElements != null) { + this.headerElements.addAll(headerElements); + } + } + + public String getTarget() { + return target; + } + + /** + * Return the optional stream identifier used for XEP-0095: Stream Initiation. + * + * @return stream identifier or null + */ + public String getSid() { + return sid; + } + + public List getAuthParameters() { + return authParamElements; + } + + public List getCookies() { + return cookieElements; + } + + public List getHeaders() { + return headerElements; + } + + private List getMetaInformationElements() { + List elements = new ArrayList<>(); + elements.addAll(getAuthParameters()); + elements.addAll(getCookies()); + elements.addAll(getHeaders()); + return elements; + } + + @Override + public XmlStringBuilder toXML(XmlEnvironment xmlEnvironment) { + List metaInformation = getMetaInformationElements(); + + XmlStringBuilder sb = new XmlStringBuilder(this); + if (!metaInformation.isEmpty()) { + sb.attribute(XMLNS_HTTP, SCHEME_HTTP); + } + sb.attribute(ATTR_TARGET, getTarget()) + .optAttribute(ATTR_SID, getSid()); + if (metaInformation.isEmpty()) { + return sb.closeEmptyElement(); + } else { + return sb.rightAngleBracket() + .append(metaInformation) + .closeElement(this); + } + } + + @Override + public String getNamespace() { + return NAMESPACE; + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public int hashCode() { + return HashCode.builder() + .append(getElementName()) + .append(getNamespace()) + .append(getTarget()) + .append(getAuthParameters()) + .append(getCookies()) + .append(getHeaders()) + .build(); + } + + @Override + public boolean equals(Object obj) { + return EqualsUtil.equals(this, obj, (equalsBuilder, other) -> + equalsBuilder + .append(getElementName(), other.getElementName()) + .append(getNamespace(), other.getNamespace()) + .append(getTarget(), other.getTarget()) + .append(getAuthParameters(), other.getAuthParameters()) + .append(getCookies(), other.getCookies()) + .append(getHeaders(), other.getHeaders())); + } +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/element/package-info.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/element/package-info.java new file mode 100644 index 000000000..0682aff01 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/element/package-info.java @@ -0,0 +1,23 @@ +/** + * + * 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. + */ + +/** + * Element classes for XEP-0103: URL Address Information. + * + * @see XEP-0103 - URL Address Information. + */ +package org.jivesoftware.smackx.urldata.element; diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/http/element/AuthParamElement.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/http/element/AuthParamElement.java new file mode 100644 index 000000000..42ca5218a --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/http/element/AuthParamElement.java @@ -0,0 +1,77 @@ +/** + * + * 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.urldata.http.element; + +import org.jivesoftware.smack.packet.XmlEnvironment; +import org.jivesoftware.smack.util.EqualsUtil; +import org.jivesoftware.smack.util.HashCode; +import org.jivesoftware.smack.util.XmlStringBuilder; + +public class AuthParamElement extends NameValuePairElement { + + public static final String ELEMENT = "auth-param"; + public static final String PREFIX = "http"; + + public static final String NAME_REALM = "realm"; + public static final String NAME_USERNAME = "username"; + public static final String NAME_PASSWORD = "password"; + + public AuthParamElement(String name, String value) { + super(name, value); + } + + public static AuthParamElement realm(String realm) { + return new AuthParamElement(NAME_REALM, realm); + } + + public static AuthParamElement username(String username) { + return new AuthParamElement(NAME_USERNAME, username); + } + + public static AuthParamElement password(String password) { + return new AuthParamElement(NAME_PASSWORD, password); + } + + @Override + public XmlStringBuilder toXML(XmlEnvironment xmlEnvironment) { + return addCommonXml(new XmlStringBuilder(this)) + .closeEmptyElement(); + } + + @Override + public String getElementName() { + return PREFIX + ':' + ELEMENT; + } + + @Override + public int hashCode() { + return HashCode.builder() + .append(getElementName()) + .append(getName()) + .append(getValue()) + .build(); + } + + @Override + public boolean equals(Object obj) { + return EqualsUtil.equals(this, obj, (equalsBuilder, other) -> + equalsBuilder + .append(getElementName(), other.getElementName()) + .append(getName(), other.getName()) + .append(getValue(), other.getValue())); + } +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/http/element/CookieElement.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/http/element/CookieElement.java new file mode 100644 index 000000000..3bd95347b --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/http/element/CookieElement.java @@ -0,0 +1,128 @@ +/** + * + * 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.urldata.http.element; + +import org.jivesoftware.smack.packet.XmlEnvironment; +import org.jivesoftware.smack.util.EqualsUtil; +import org.jivesoftware.smack.util.HashCode; +import org.jivesoftware.smack.util.XmlStringBuilder; + +public class CookieElement extends NameValuePairElement { + + public static final String ELEMENT = "cookie"; + public static final String PREFIX = "http"; + public static final String ATTR_DOMAIN = "domain"; + public static final String ATTR_MAX_AGE = "max-age"; + public static final String ATTR_PATH = "path"; + public static final String ATTR_COMMENT = "comment"; + public static final String ATTR_VERSION = "version"; + public static final String ATTR_SECURE = "secure"; + + private final String domain; + private final Integer maxAge; + private final String path; + private final String comment; + private final String version; + private final Boolean secure; + + public CookieElement(String name, String value) { + this(name, value, null, null, null, null, null, null); + } + + public CookieElement(String name, String value, String domain, Integer maxAge, String path, String comment, String version, Boolean secure) { + super(name, value); + this.domain = domain; + this.maxAge = maxAge; + this.path = path; + this.comment = comment; + this.version = version; + this.secure = secure; + } + + public String getPath() { + return path; + } + + public int getMaxAge() { + return maxAge == null ? 0 : maxAge; + } + + public String getDomain() { + return domain; + } + + public String getComment() { + return comment; + } + + public String getVersion() { + return version == null ? "1.0" : version; + } + + public boolean isSecure() { + return secure != null && secure; + } + + @Override + public XmlStringBuilder toXML(XmlEnvironment xmlEnvironment) { + XmlStringBuilder sb = addCommonXml(new XmlStringBuilder(this)) + .optAttribute(ATTR_DOMAIN, domain) + .optAttribute(ATTR_MAX_AGE, maxAge) + .optAttribute(ATTR_PATH, path) + .optAttribute(ATTR_COMMENT, comment) + .optAttribute(ATTR_VERSION, version); + if (secure != null) { + sb.attribute(ATTR_SECURE, secure); + } + return sb.closeEmptyElement(); + } + + @Override + public String getElementName() { + return PREFIX + ':' + ELEMENT; + } + + @Override + public int hashCode() { + return HashCode.builder() + .append(getElementName()) + .append(getName()) + .append(getValue()) + .append(getDomain()) + .append(getMaxAge()) + .append(getPath()) + .append(getComment()) + .append(getVersion()) + .append(isSecure()) + .build(); + } + + @Override + public boolean equals(Object obj) { + return EqualsUtil.equals(this, obj, (equalsBuilder, other) -> + equalsBuilder + .append(getElementName(), other.getElementName()) + .append(getName(), other.getName()) + .append(getValue(), other.getValue()) + .append(getDomain(), other.getDomain()) + .append(getMaxAge(), other.getMaxAge()) + .append(getPath(), other.getPath()) + .append(getComment(), other.getComment()) + .append(getVersion(), other.getVersion()) + .append(isSecure(), other.isSecure())); + } +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/http/element/HeaderElement.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/http/element/HeaderElement.java new file mode 100644 index 000000000..0feb816e3 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/http/element/HeaderElement.java @@ -0,0 +1,61 @@ +/** + * + * 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.urldata.http.element; + +import org.jivesoftware.smack.packet.XmlEnvironment; +import org.jivesoftware.smack.util.EqualsUtil; +import org.jivesoftware.smack.util.HashCode; +import org.jivesoftware.smack.util.XmlStringBuilder; + +public class HeaderElement extends NameValuePairElement { + + public static final String ELEMENT = "header"; + public static final String PREFIX = "http"; + + public HeaderElement(String name, String value) { + super(name, value); + } + + @Override + public XmlStringBuilder toXML(XmlEnvironment xmlEnvironment) { + return addCommonXml(new XmlStringBuilder(this)) + .closeEmptyElement(); + } + + @Override + public String getElementName() { + return PREFIX + ':' + ELEMENT; + } + + @Override + public int hashCode() { + return HashCode.builder() + .append(getElementName()) + .append(getName()) + .append(getValue()) + .build(); + } + + @Override + public boolean equals(Object obj) { + return EqualsUtil.equals(this, obj, (equalsBuilder, other) -> + equalsBuilder + .append(getElementName(), other.getElementName()) + .append(getName(), other.getName()) + .append(getValue(), other.getValue())); + } +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/http/element/HttpAuthElement.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/http/element/HttpAuthElement.java new file mode 100644 index 000000000..ff11163f2 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/http/element/HttpAuthElement.java @@ -0,0 +1,123 @@ +/** + * + * 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.urldata.http.element; + +import java.util.ArrayList; +import java.util.List; + +import org.jivesoftware.smack.packet.XmlEnvironment; +import org.jivesoftware.smack.util.EqualsUtil; +import org.jivesoftware.smack.util.HashCode; +import org.jivesoftware.smack.util.XmlStringBuilder; + +import org.jivesoftware.smackx.urldata.element.MetaInformationElement; + +public final class HttpAuthElement implements MetaInformationElement { + + public static final String ELEMENT = "auth"; + public static final String PREFIX = "http"; + public static final String ATTR_SCHEME = "scheme"; + + public static final String SCHEME_BASIC = "basic"; + + private final String scheme; + private final List params = new ArrayList<>(); + + public HttpAuthElement(String scheme, List params) { + this.scheme = scheme; + if (params != null) { + this.params.addAll(params); + } + } + + public static HttpAuthElement basicAuth() { + return basicAuth(null, null); + } + + public static HttpAuthElement basicAuth(String username, String password) { + return basicAuth(null, username, password); + } + + public static HttpAuthElement basicAuth(String realm, String username, String password) { + List params = new ArrayList<>(); + if (realm != null) { + params.add(AuthParamElement.realm(realm)); + } + if (username != null) { + params.add(AuthParamElement.username(username)); + } + if (password != null) { + params.add(AuthParamElement.password(password)); + } + + return new HttpAuthElement(SCHEME_BASIC, params); + } + + public String getScheme() { + return scheme; + } + + public List getParams() { + return params; + } + + @Override + public XmlStringBuilder toXML(XmlEnvironment xmlEnvironment) { + XmlStringBuilder sb = new XmlStringBuilder(this) + .attribute(ATTR_SCHEME, getScheme()); + if (getParams().isEmpty()) { + return sb.closeEmptyElement(); + } else { + return sb.rightAngleBracket() + .append(getParams()) + .closeElement(this); + } + } + + @Override + public String getElementName() { + return PREFIX + ':' + ELEMENT; + } + + public AuthParamElement getParam(String name) { + for (AuthParamElement param : getParams()) { + if (param.getName().equals(name)) { + return param; + } + } + return null; + } + + @Override + public int hashCode() { + return HashCode.builder() + .append(getElementName()) + .append(getScheme()) + .append(getParams()) + .build(); + } + + @Override + public boolean equals(Object obj) { + return EqualsUtil.equals(this, obj, (equalsBuilder, other) -> + equalsBuilder + .append(getElementName(), other.getElementName()) + .append(getScheme(), other.getScheme()) + .append(getParams(), other.getParams())); + } + +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/http/element/NameValuePairElement.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/http/element/NameValuePairElement.java new file mode 100644 index 000000000..911bdb339 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/http/element/NameValuePairElement.java @@ -0,0 +1,49 @@ +/** + * + * 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.urldata.http.element; + +import org.jivesoftware.smack.util.XmlStringBuilder; + +import org.jivesoftware.smackx.urldata.element.MetaInformationElement; + +public abstract class NameValuePairElement implements MetaInformationElement { + + public static final String ATTR_NAME = "name"; + public static final String ATTR_VALUE = "value"; + + private final String name; + private final String value; + + public NameValuePairElement(String name, String value) { + this.name = name; + this.value = value; + } + + public String getName() { + return name; + } + + public String getValue() { + return value; + } + + public XmlStringBuilder addCommonXml(XmlStringBuilder sb) { + return sb.attribute(ATTR_NAME, getName()) + .attribute(ATTR_VALUE, getValue()); + } + +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/http/element/package-info.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/http/element/package-info.java new file mode 100644 index 000000000..7bc42714a --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/http/element/package-info.java @@ -0,0 +1,21 @@ +/** + * + * 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. + */ + +/** + * Element classes for XEP-0104. + */ +package org.jivesoftware.smackx.urldata.http.element; diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/http/package-info.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/http/package-info.java new file mode 100644 index 000000000..4a7f1b8db --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/http/package-info.java @@ -0,0 +1,23 @@ +/** + * + * 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. + */ + +/** + * Smack's API for XEP-0104: HTTP Scheme for URL Address Information. + * + * @see XEP-0104 - HTTP Scheme for URL Address Information + */ +package org.jivesoftware.smackx.urldata.http; diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/package-info.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/package-info.java new file mode 100644 index 000000000..06fec0af9 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/package-info.java @@ -0,0 +1,21 @@ +/** + * + * 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. + */ + +/** + * Smack's API for XEP-0103 - URL Address Information. + */ +package org.jivesoftware.smackx.urldata; diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/provider/UrlDataElementProvider.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/provider/UrlDataElementProvider.java new file mode 100644 index 000000000..f1a8a2b25 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/provider/UrlDataElementProvider.java @@ -0,0 +1,101 @@ +/** + * + * 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.urldata.provider; + +import static org.jivesoftware.smackx.urldata.element.UrlDataElement.ATTR_SID; +import static org.jivesoftware.smackx.urldata.element.UrlDataElement.ATTR_TARGET; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.jivesoftware.smack.packet.XmlEnvironment; +import org.jivesoftware.smack.parsing.SmackParsingException; +import org.jivesoftware.smack.provider.ExtensionElementProvider; +import org.jivesoftware.smack.util.ParserUtils; +import org.jivesoftware.smack.xml.XmlPullParser; +import org.jivesoftware.smack.xml.XmlPullParserException; + +import org.jivesoftware.smackx.urldata.element.UrlDataElement; +import org.jivesoftware.smackx.urldata.http.element.AuthParamElement; +import org.jivesoftware.smackx.urldata.http.element.CookieElement; +import org.jivesoftware.smackx.urldata.http.element.HeaderElement; +import org.jivesoftware.smackx.urldata.http.element.HttpAuthElement; + +public class UrlDataElementProvider extends ExtensionElementProvider { + + @Override + public UrlDataElement parse(XmlPullParser parser, int initialDepth, XmlEnvironment xmlEnvironment) throws XmlPullParserException, IOException, SmackParsingException { + String target = parser.getAttributeValue(ATTR_TARGET); + String sid = parser.getAttributeValue(ATTR_SID); + List authElements = new ArrayList<>(); + List cookieElements = new ArrayList<>(); + List headerElements = new ArrayList<>(); + do { + XmlPullParser.TagEvent event = parser.nextTag(); + String name = parser.getName(); + + if (event == XmlPullParser.TagEvent.START_ELEMENT) { + switch (name) { + case UrlDataElement.ELEMENT: + continue; + + case HttpAuthElement.ELEMENT: + String scheme = parser.getAttributeValue(HttpAuthElement.ATTR_SCHEME); + List authParamElements = new ArrayList<>(); + int innerDepth = parser.getDepth(); + do { + XmlPullParser.TagEvent innerTag = parser.nextTag(); + String innerName = parser.getName(); + if (innerTag.equals(XmlPullParser.TagEvent.START_ELEMENT)) { + if (innerName.equals(AuthParamElement.ELEMENT)) { + String attrName = ParserUtils.getRequiredAttribute(parser, AuthParamElement.ATTR_NAME); + String attrVal = ParserUtils.getRequiredAttribute(parser, AuthParamElement.ATTR_VALUE); + authParamElements.add(new AuthParamElement(attrName, attrVal)); + } + } + } while (parser.getDepth() != innerDepth); + + authElements.add(new HttpAuthElement(scheme, authParamElements)); + break; + + case CookieElement.ELEMENT: + String cookieName = ParserUtils.getRequiredAttribute(parser, CookieElement.ATTR_NAME); + String cookieValue = ParserUtils.getRequiredAttribute(parser, CookieElement.ATTR_VALUE); + String cookieDomain = parser.getAttributeValue(CookieElement.ATTR_DOMAIN); + Integer cookieMaxAge = ParserUtils.getIntegerAttribute(parser, CookieElement.ATTR_MAX_AGE); + String cookiePath = parser.getAttributeValue(CookieElement.ATTR_PATH); + String cookieComment = parser.getAttributeValue(CookieElement.ATTR_COMMENT); + Boolean cookieSecure = ParserUtils.getBooleanAttribute(parser, CookieElement.ATTR_SECURE); + String cookieVersion = parser.getAttributeValue(CookieElement.ATTR_VERSION); + + cookieElements.add(new CookieElement(cookieName, cookieValue, cookieDomain, cookieMaxAge, cookiePath, cookieComment, cookieVersion, cookieSecure)); + break; + + case HeaderElement.ELEMENT: + String headerName = ParserUtils.getRequiredAttribute(parser, HeaderElement.ATTR_NAME); + String headerValue = ParserUtils.getRequiredAttribute(parser, HeaderElement.ATTR_VALUE); + + headerElements.add(new HeaderElement(headerName, headerValue)); + break; + } + } + } while (parser.getDepth() != initialDepth); + + return new UrlDataElement(target, sid, authElements, cookieElements, headerElements); + } +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/provider/package-info.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/provider/package-info.java new file mode 100644 index 000000000..c31c44db0 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/urldata/provider/package-info.java @@ -0,0 +1,23 @@ +/** + * + * 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. + */ + +/** + * Provider classes for XEP-0103: URL Address Information. + * + * @see XEP-0103 - URL Address Information. + */ +package org.jivesoftware.smackx.urldata.provider; diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/vcardtemp/VCardManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/vcardtemp/VCardManager.java index 0bfbba9b2..5aefdeeff 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/vcardtemp/VCardManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/vcardtemp/VCardManager.java @@ -106,7 +106,7 @@ public final class VCardManager extends Manager { vcard.setType(IQ.Type.set); // Also make sure to generate a new stanza id (the given vcard could be a vcard result), in which case we don't // want to use the same stanza id again (although it wouldn't break if we did) - vcard.setStanzaId(); + // vcard.setStanzaId(); TODO FIXME connection().sendIqRequestAndWaitForResponse(vcard); } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/vcardtemp/packet/VCard.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/vcardtemp/packet/VCard.java index 4f4d74a19..4d6f18ecf 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/vcardtemp/packet/VCard.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/vcardtemp/packet/VCard.java @@ -122,7 +122,7 @@ public final class VCard extends IQ { private String photoBinval; /** - * Such as DESC ROLE GEO etc.. see XEP-0054 + * Such as DESC ROLE GEO etc. see XEP-0054 */ private final Map otherSimpleFields = new HashMap<>(); @@ -575,7 +575,7 @@ public final class VCard extends IQ { * Load VCard information for a given user. XMPPConnection should be authenticated and not anonymous. * * @param connection connection. - * @param user user whos information we want to load. + * @param user user whose information we want to load. * * @throws XMPPErrorException if there was an XMPP error returned. * @throws NoResponseException if there was no response from the server. diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/BooleanFormField.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/BooleanFormField.java index 6681e6408..756c14b52 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/BooleanFormField.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/BooleanFormField.java @@ -36,7 +36,7 @@ public class BooleanFormField extends SingleValueFormField { } /** - * Get the value of the booelan field. Note that, if no explicit boolean value is provided, in the form of "true", + * Get the value of the boolean field. Note that, if no explicit boolean value is provided, in the form of "true", * "false", "0", or "1", then the default value of a boolean field is false, according to * XEP-0004 § 3.3. * diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/FormField.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/FormField.java index a2ad8cb76..bd172be54 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/FormField.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/FormField.java @@ -1,6 +1,6 @@ /** * - * Copyright 2003-2007 Jive Software, 2019-2021 Florian Schmaus. + * Copyright 2003-2007 Jive Software, 2019-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. @@ -342,23 +342,6 @@ public abstract class FormField implements XmlElement { return XmppDateTime.parseXEP0082Date(valueString); } - /** - * Returns the field's name, also known as the variable name in case this is an filled out answer form. - *

    - * According to XEP-4 § 3.2 the variable name (the 'var' attribute) - * "uniquely identifies the field in the context of the form" (if the field is not of type 'fixed', in which case - * the field "MAY possess a 'var' attribute") - *

    - * - * @return the field's name. - * @deprecated use {@link #getFieldName()} instead. - */ - // TODO: Remove in Smack 4.5 - @Deprecated - public String getVariable() { - return getFieldName(); - } - /** * Returns the field's name, also known as the variable name in case this is an filled out answer form. *

    @@ -403,7 +386,7 @@ public abstract class FormField implements XmlElement { protected transient List extraXmlChildElements; /** - * Populate @{link {@link #extraXmlChildElements}}. Note that this method may be overridden by subclasses. + * Populate {@link #extraXmlChildElements}. Note that this method may be overridden by subclasses. */ protected void populateExtraXmlChildElements() { List values = getRawValues(); diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/form/FillableForm.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/form/FillableForm.java index 159c20544..302156c2c 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/form/FillableForm.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/form/FillableForm.java @@ -1,6 +1,6 @@ /** * - * Copyright 2020 Florian Schmaus + * Copyright 2020-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. @@ -30,6 +30,8 @@ import org.jivesoftware.smackx.xdata.AbstractMultiFormField; import org.jivesoftware.smackx.xdata.AbstractSingleStringValueFormField; import org.jivesoftware.smackx.xdata.FormField; import org.jivesoftware.smackx.xdata.FormFieldChildElement; +import org.jivesoftware.smackx.xdata.ListMultiFormField; +import org.jivesoftware.smackx.xdata.ListSingleFormField; import org.jivesoftware.smackx.xdata.packet.DataForm; import org.jxmpp.jid.Jid; @@ -47,6 +49,7 @@ public class FillableForm extends FilledForm { private final Map filledFields = new HashMap<>(); + @SuppressWarnings("this-escape") public FillableForm(DataForm dataForm) { super(dataForm); if (dataForm.getType() != DataForm.Type.form) { @@ -54,6 +57,7 @@ public class FillableForm extends FilledForm { } Set requiredFields = new HashSet<>(); + List requiredFieldsWithDefaultValue = new ArrayList<>(); for (FormField formField : dataForm.getFields()) { if (formField.isRequired()) { String fieldName = formField.getFieldName(); @@ -61,13 +65,17 @@ public class FillableForm extends FilledForm { if (formField.hasValueSet()) { // This is a form field with a default value. - write(formField); + requiredFieldsWithDefaultValue.add(formField); } else { missingRequiredFields.add(fieldName); } } } this.requiredFields = Collections.unmodifiableSet(requiredFields); + + for (FormField field : requiredFieldsWithDefaultValue) { + write(field); + } } protected void writeListMulti(String fieldName, List values) { @@ -221,11 +229,20 @@ public class FillableForm extends FilledForm { if (filledFormField.getType() == FormField.Type.fixed) { throw new IllegalArgumentException(); } - if (!filledFormField.hasValueSet()) { - throw new IllegalArgumentException(); - } String fieldName = filledFormField.getFieldName(); + + boolean isListField = filledFormField instanceof ListMultiFormField + || filledFormField instanceof ListSingleFormField; + // Only list-* fields require a value to be set. Other fields types can be empty. For example MUC's + // muc#roomconfig_roomadmins, which is of type jid-multi, is submitted without values to reset the room's admin + // list. + if (isListField && !filledFormField.hasValueSet()) { + throw new IllegalArgumentException("Tried to write form field " + fieldName + " of type " + + filledFormField.getType() + + " without any values set. However, according to XEP-0045 § 3.3 fields of type list-multi or list-single must have one item set."); + } + if (!getDataForm().hasField(fieldName)) { throw new IllegalArgumentException(); } @@ -270,4 +287,8 @@ public class FillableForm extends FilledForm { return builder.build(); } + public SubmitForm getSubmitForm() { + DataForm form = getDataFormToSubmit(); + return new SubmitForm(form); + } } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/form/SubmitForm.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/form/SubmitForm.java new file mode 100644 index 000000000..f317dda27 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/form/SubmitForm.java @@ -0,0 +1,31 @@ +/** + * + * 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.xdata.form; + +import org.jivesoftware.smackx.xdata.packet.DataForm; + +public class SubmitForm extends FilledForm { + + public SubmitForm(DataForm dataForm) { + super(dataForm); + + if (dataForm.getType() != DataForm.Type.submit) { + throw new IllegalArgumentException(); + } + } + +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/packet/DataForm.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/packet/DataForm.java index 9ab354d57..e9f2da625 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/packet/DataForm.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/packet/DataForm.java @@ -1,6 +1,6 @@ /** * - * Copyright 2003-2007 Jive Software, 2020-2021 Florian Schmaus. + * Copyright 2003-2007 Jive Software, 2020-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. @@ -33,7 +33,6 @@ import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.StanzaView; import org.jivesoftware.smack.packet.XmlEnvironment; import org.jivesoftware.smack.util.CollectionUtil; -import org.jivesoftware.smack.util.Objects; import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.util.XmlStringBuilder; @@ -356,8 +355,7 @@ public final class DataForm implements ExtensionElement { } public static final class Builder { - // TODO: Make this field final once setType() is gone. - private Type type; + private final Type type; private String title; private List instructions; private ReportedData reportedData; @@ -409,20 +407,6 @@ public final class DataForm implements ExtensionElement { } } - /** - * Deprecated do not use. - * - * @param type the type. - * @return a reference to this builder. - * @deprecated use {@link DataForm#builder(Type)} instead. - */ - @Deprecated - // TODO: Remove in Smack 4.5 and then make this.type final. - public Builder setType(Type type) { - this.type = Objects.requireNonNull(type); - return this; - } - /** * Sets the description of the data. It is similar to the title on a web page or an X window. * You can put a <title/> on either a form to fill out, or a set of data results. diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/xhtmlim/package-info.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/xhtmlim/package-info.java index 7054e0f22..7ebb8cee9 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/xhtmlim/package-info.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/xhtmlim/package-info.java @@ -152,7 +152,7 @@ * Before you start to send XHTML messages to a user you should discover if the user supports XHTML messages. There are * two ways to achieve the discovery, explicitly and implicitly. Explicit is when you first try to discover if the user * supports XHTML before sending any XHTML message. Implicit is when you send XHTML messages without first discovering - * if the conversation partner's client supports XHTML and depenging on the answer (normal message or XHTML message) you + * if the conversation partner's client supports XHTML and depending on the answer (normal message or XHTML message) you * find out if the user supports XHTML messages or not. This section explains how to explicitly discover for XHTML * support. *

    diff --git a/smack-extensions/src/main/resources/org.jivesoftware.smack.extensions/extensions.providers b/smack-extensions/src/main/resources/org.jivesoftware.smack.extensions/extensions.providers index c2ac6921e..8dd5b32d3 100644 --- a/smack-extensions/src/main/resources/org.jivesoftware.smack.extensions/extensions.providers +++ b/smack-extensions/src/main/resources/org.jivesoftware.smack.extensions/extensions.providers @@ -365,6 +365,13 @@ org.jivesoftware.smackx.si.provider.StreamInitiationProvider + + + url-data + http://jabber.org/protocol/url-data + org.jivesoftware.smackx.urldata.provider.UrlDataElementProvider + + mood diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5ByteStreamManagerTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5ByteStreamManagerTest.java index 00e721e5b..0c59cf73c 100644 --- a/smack-extensions/src/test/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5ByteStreamManagerTest.java +++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5ByteStreamManagerTest.java @@ -179,7 +179,7 @@ public class Socks5ByteStreamManagerTest { Socks5BytestreamManager byteStreamManager = Socks5BytestreamManager.getBytestreamManager(connection); byteStreamManager.setAnnounceLocalStreamHost(false); - /** + /* * create responses in the order they should be queried specified by the XEP-0065 * specification */ @@ -231,7 +231,7 @@ public class Socks5ByteStreamManagerTest { Socks5BytestreamManager byteStreamManager = Socks5BytestreamManager.getBytestreamManager(connection); byteStreamManager.setAnnounceLocalStreamHost(false); - /** + /* * create responses in the order they should be queried specified by the XEP-0065 * specification */ @@ -292,7 +292,7 @@ public class Socks5ByteStreamManagerTest { Socks5BytestreamManager byteStreamManager = Socks5BytestreamManager.getBytestreamManager(connection); byteStreamManager.setAnnounceLocalStreamHost(false); - /** + /* * create responses in the order they should be queried specified by the XEP-0065 * specification */ @@ -375,7 +375,7 @@ public class Socks5ByteStreamManagerTest { Socks5BytestreamManager byteStreamManager = Socks5BytestreamManager.getBytestreamManager(connection); byteStreamManager.setAnnounceLocalStreamHost(false); - /** + /* * create responses in the order they should be queried specified by the XEP-0065 * specification */ @@ -454,7 +454,7 @@ public class Socks5ByteStreamManagerTest { // TODO: It appears that it is not required to disable the local stream host for this unit test. byteStreamManager.setAnnounceLocalStreamHost(false); - /** + /* * create responses in the order they should be queried specified by the XEP-0065 * specification */ @@ -541,7 +541,7 @@ public class Socks5ByteStreamManagerTest { byteStreamManager.setAnnounceLocalStreamHost(false); byteStreamManager.setProxyConnectionTimeout(3000); - /** + /* * create responses in the order they should be queried specified by the XEP-0065 * specification */ @@ -634,7 +634,7 @@ public class Socks5ByteStreamManagerTest { Socks5BytestreamManager byteStreamManager = Socks5BytestreamManager.getBytestreamManager(connection); byteStreamManager.setAnnounceLocalStreamHost(false); - /** + /* * create responses in the order they should be queried specified by the XEP-0065 * specification */ @@ -760,7 +760,7 @@ public class Socks5ByteStreamManagerTest { // get Socks5ByteStreamManager for connection Socks5BytestreamManager byteStreamManager = Socks5BytestreamManager.getBytestreamManager(connection); - /** + /* * create responses in the order they should be queried specified by the XEP-0065 * specification */ diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/caps/EntityCapsManagerTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/caps/EntityCapsManagerTest.java index 87700708e..d9a0666c3 100644 --- a/smack-extensions/src/test/java/org/jivesoftware/smackx/caps/EntityCapsManagerTest.java +++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/caps/EntityCapsManagerTest.java @@ -22,8 +22,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.File; import java.io.IOException; +import java.util.ArrayList; import java.util.Collection; -import java.util.LinkedList; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.test.util.SmackTestSuite; @@ -47,6 +47,41 @@ import org.jxmpp.stringprep.XmppStringprepException; public class EntityCapsManagerTest extends SmackTestSuite { + /** + * XEP- + * 0115 Simple Generation Example. + * @throws XmppStringprepException if the provided string is invalid. + */ + @Test + public void testSimpleGenerationExample() throws XmppStringprepException { + DiscoverInfo di = createSimpleSamplePacket(); + + CapsVersionAndHash versionAndHash = EntityCapsManager.generateVerificationString(di, StringUtils.SHA1); + assertEquals("QgayPKawpkPSDYmwT/WM94uAlu0=", versionAndHash.version); + } + + /** + * Asserts that the order in which data forms are present in the disco/info does not affect the calculated + * verification string, as the XEP mandates that these are ordered by FORM_TYPE (i.e., by the XML character data of + * the element). + * @throws XmppStringprepException if the provided string is invalid. + */ + @Test + public void testReversedDataFormOrder() throws XmppStringprepException { + final DiscoverInfoBuilder builderA = createSimpleSampleBuilder(); + builderA.addExtension(createSampleServerInfoDataForm()); // This works, as the underlying MultiMap maintains insertion-order. + builderA.addExtension(createSampleSoftwareInfoDataForm()); + + final DiscoverInfoBuilder builderB = createSimpleSampleBuilder(); + builderB.addExtension(createSampleSoftwareInfoDataForm()); + builderB.addExtension(createSampleServerInfoDataForm()); + + CapsVersionAndHash versionAndHashA = EntityCapsManager.generateVerificationString(builderA.build(), StringUtils.SHA1); + CapsVersionAndHash versionAndHashB = EntityCapsManager.generateVerificationString(builderB.build(), StringUtils.SHA1); + + assertEquals(versionAndHashA.version, versionAndHashB.version); + } + /** * XEP- * 0115 Complex Generation Example. @@ -142,13 +177,48 @@ public class EntityCapsManagerTest extends SmackTestSuite { return df.build(); } + private static DataForm createSampleServerInfoDataForm() { + DataForm.Builder df = DataForm.builder(DataForm.Type.result); + + { + TextMultiFormField.Builder ff = FormField.textMultiBuilder("admin-addresses"); + ff.addValue("xmpp:admin@example.org"); + ff.addValue("mailto:admin@example.com"); + df.addField(ff.build()); + } + + { + TextSingleFormField.Builder ff = FormField.hiddenBuilder("FORM_TYPE"); + ff.setValue("http://jabber.org/network/serverinfo"); + df.addField(ff.build()); + } + + return df.build(); + } + + private static DiscoverInfoBuilder createSimpleSampleBuilder() throws XmppStringprepException { + DiscoverInfoBuilder di = DiscoverInfo.builder("disco1"); + di.ofType(IQ.Type.result); + + di.addIdentity(new DiscoverInfo.Identity("client", "Exodus 0.9.1", "pc")); + di.addFeature("http://jabber.org/protocol/disco#info"); + di.addFeature("http://jabber.org/protocol/disco#items"); + di.addFeature("http://jabber.org/protocol/muc"); + di.addFeature("http://jabber.org/protocol/caps"); + + return di; + } + private static DiscoverInfo createSimpleSamplePacket() throws XmppStringprepException { + return createSimpleSampleBuilder().build(); + } + private static DiscoverInfo createComplexSamplePacket() throws XmppStringprepException { DiscoverInfoBuilder di = DiscoverInfo.builder("disco1"); di.from(JidCreate.from("benvolio@capulet.lit/230193")); di.to(JidCreate.from("juliet@capulet.lit/chamber")); di.ofType(IQ.Type.result); - Collection identities = new LinkedList(); + Collection identities = new ArrayList(); DiscoverInfo.Identity i = new DiscoverInfo.Identity("client", "pc", "Psi 0.11", "en"); identities.add(i); i = new DiscoverInfo.Identity("client", "pc", "Ψ 0.11", "el"); @@ -171,7 +241,7 @@ public class EntityCapsManagerTest extends SmackTestSuite { di.to(")juliet@capulet.lit/chamber"); di.ofType(IQ.Type.result); - Collection identities = new LinkedList(); + Collection identities = new ArrayList(); DiscoverInfo.Identity i = new DiscoverInfo.Identity("client", "pc", "Psi 0.11", "en"); identities.add(i); i = new DiscoverInfo.Identity("client", "pc", "Ψ 0.11", "el"); diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/commands/provider/CommandsProviderTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/commands/provider/CommandsProviderTest.java index 68d2098b7..d8b0e734f 100644 --- a/smack-extensions/src/test/java/org/jivesoftware/smackx/commands/provider/CommandsProviderTest.java +++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/commands/provider/CommandsProviderTest.java @@ -23,7 +23,6 @@ import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.packet.StanzaError; import org.jivesoftware.smack.util.PacketParserUtils; -import org.jivesoftware.smackx.commands.AdHocCommand; import org.jivesoftware.smackx.commands.packet.AdHocCommandData; import org.junit.jupiter.api.Test; @@ -41,7 +40,7 @@ public class CommandsProviderTest { final AdHocCommandData adHocIq = (AdHocCommandData) requestStanza; assertEquals(IQ.Type.error, adHocIq.getType()); - assertEquals(AdHocCommand.Action.execute, adHocIq.getAction()); + assertEquals(AdHocCommandData.Action.execute, adHocIq.getAction()); StanzaError error = adHocIq.getError(); assertEquals(StanzaError.Type.CANCEL, error.getType()); diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/muc/packet/GroupChatInvitationElementTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/muc/packet/GroupChatInvitationElementTest.java new file mode 100644 index 000000000..1f28be5ce --- /dev/null +++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/muc/packet/GroupChatInvitationElementTest.java @@ -0,0 +1,69 @@ +/** + * + * 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.packet; + +import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; + +import org.jivesoftware.smack.parsing.SmackParsingException; +import org.jivesoftware.smack.test.util.TestUtils; +import org.jivesoftware.smack.xml.XmlPullParserException; +import org.jivesoftware.smackx.muc.provider.GroupChatInvitationProvider; + +import org.junit.jupiter.api.Test; +import org.jxmpp.jid.EntityBareJid; +import org.jxmpp.jid.impl.JidCreate; + +public class GroupChatInvitationElementTest { + private static final GroupChatInvitationProvider TEST_PROVIDER = new GroupChatInvitationProvider(); + + private static final EntityBareJid mucJid = JidCreate.entityBareFromOrThrowUnchecked("darkcave@macbeth.shakespeare.lit"); + + @Test + public void serializeFullElement() throws XmlPullParserException, IOException, SmackParsingException { + final String expectedXml = "" + + ""; + + 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/urldata/UrlDataElementTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/urldata/UrlDataElementTest.java new file mode 100644 index 000000000..72bc4f32a --- /dev/null +++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/urldata/UrlDataElementTest.java @@ -0,0 +1,219 @@ +/** + * + * 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.urldata; + +import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; + +import org.jivesoftware.smack.parsing.SmackParsingException; +import org.jivesoftware.smack.test.util.SmackTestSuite; +import org.jivesoftware.smack.test.util.TestUtils; +import org.jivesoftware.smack.xml.XmlPullParserException; + +import org.jivesoftware.smackx.urldata.element.UrlDataElement; +import org.jivesoftware.smackx.urldata.http.element.CookieElement; +import org.jivesoftware.smackx.urldata.http.element.HeaderElement; +import org.jivesoftware.smackx.urldata.http.element.HttpAuthElement; +import org.jivesoftware.smackx.urldata.provider.UrlDataElementProvider; + +import org.junit.jupiter.api.Test; + +public class UrlDataElementTest extends SmackTestSuite { + + public static final UrlDataElementProvider URL_DATA_ELEMENT_PROVIDER = new UrlDataElementProvider(); + + @Test + public void simpleSerializationTest() throws XmlPullParserException, IOException, SmackParsingException { + UrlDataElement urlDataElement = new UrlDataElement("http://www.jabber.org/members/index.php", + null, + Collections.singletonList(HttpAuthElement.basicAuth()), + null, null); + + final String expectedXml = "" + + "" + + "" + + ""; + assertXmlSimilar(expectedXml, urlDataElement.toXML().toString()); + + UrlDataElement parsed = URL_DATA_ELEMENT_PROVIDER.parse(TestUtils.getParser(expectedXml)); + assertEquals(urlDataElement, parsed); + } + + @Test + public void additionalAuthParamTest() throws XmlPullParserException, IOException, SmackParsingException { + + UrlDataElement urlDataElement = new UrlDataElement("http://www.jabber.org/members/index.php", + null, + Collections.singletonList(HttpAuthElement.basicAuth( + "www.jabber.org", + "defaultuser", + "defaultpwd" + )), + null, + null); + + final String expectedXml = "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " "; + assertXmlSimilar(expectedXml, urlDataElement.toXML().toString()); + + UrlDataElement parsed = URL_DATA_ELEMENT_PROVIDER.parse(TestUtils.getParser(expectedXml)); + assertEquals(urlDataElement, parsed); + } + + @Test + public void simpleUrlWithSidTest() throws XmlPullParserException, IOException, SmackParsingException { + UrlDataElement urlDataElement = new UrlDataElement("http://pass.jabber.org:8519/test.txt", "a0"); + + final String expectedXml = ""; + assertXmlSimilar(expectedXml, urlDataElement.toXML().toString()); + + UrlDataElement parsed = URL_DATA_ELEMENT_PROVIDER.parse(TestUtils.getParser(expectedXml)); + assertEquals(urlDataElement, parsed); + } + + @Test + public void simpleUrlNoChildrenTest() throws XmlPullParserException, IOException, SmackParsingException { + UrlDataElement urlDataElement = new UrlDataElement("http://festhall.outer-planes.net/d20M/announce/latest/", null); + + final String expectedXml = ""; + assertXmlSimilar(expectedXml, urlDataElement.toXML().toString()); + + UrlDataElement parsed = URL_DATA_ELEMENT_PROVIDER.parse(TestUtils.getParser(expectedXml)); + assertEquals(urlDataElement, parsed); + } + + @Test + public void simpleCookieTest() throws XmlPullParserException, IOException, SmackParsingException { + UrlDataElement urlDataElement = new UrlDataElement("http://www.jabber.org/members/index.php", + null, + null, + Collections.singletonList(new CookieElement("jsessionid", "1243asd234190sa32ds")), + null); + + final String expectedXml = "" + + "\n" + + " \n" + + ""; + assertXmlSimilar(expectedXml, urlDataElement.toXML().toString()); + + UrlDataElement parsed = URL_DATA_ELEMENT_PROVIDER.parse(TestUtils.getParser(expectedXml)); + assertEquals(urlDataElement, parsed); + } + + @Test + public void additionalParametersCookieTest() throws XmlPullParserException, IOException, SmackParsingException { + UrlDataElement urlDataElement = new UrlDataElement("http://www.jabber.org/members/index.php", + null, + null, + Collections.singletonList(new CookieElement( + "jsessionid", + "1243asd234190sa32ds", + "jabber.org", + 1234000, + "/members", + "Web Session Identifier", + "1.0", + false + )), + null); + + final String expectedXml = "\n" + + " \n" + + ""; + assertXmlSimilar(expectedXml, urlDataElement.toXML().toString()); + + UrlDataElement parsed = URL_DATA_ELEMENT_PROVIDER.parse(TestUtils.getParser(expectedXml)); + assertEquals(urlDataElement, parsed); + } + + @Test + public void simpleHeaderTest() throws XmlPullParserException, IOException, SmackParsingException { + UrlDataElement urlDataElement = new UrlDataElement( + "http://www.jabber.org/members/index.php", + null, + null, + null, + Collections.singletonList(new HeaderElement("Custom-Data", "some custom data"))); + + final String expectedXml = "\n" + + " \n" + + " "; + assertXmlSimilar(expectedXml, urlDataElement.toXML().toString()); + + UrlDataElement parsed = URL_DATA_ELEMENT_PROVIDER.parse(TestUtils.getParser(expectedXml)); + assertEquals(urlDataElement, parsed); + } + + @Test + public void multiChildTest() throws XmlPullParserException, IOException, SmackParsingException { + UrlDataElement urlDataElement = new UrlDataElement( + "https://blog.jabberhead.tk", + null, + Collections.singletonList(HttpAuthElement.basicAuth()), + Arrays.asList( + new CookieElement("jsessionid", "somecookievalue"), + new CookieElement("come2darkSide", "weHaveCookies")), + Arrays.asList( + new HeaderElement("Accept", "text/plain"), + new HeaderElement("Access-Control-Allow-Origin", "*"))); + + final String expectedXml = + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " "; + assertXmlSimilar(expectedXml, urlDataElement.toXML().toString()); + + UrlDataElement parsed = URL_DATA_ELEMENT_PROVIDER.parse(TestUtils.getParser(expectedXml)); + assertEquals(urlDataElement, 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-extensions/src/testFixtures/java/org/jivesoftware/util/Protocol.java b/smack-extensions/src/testFixtures/java/org/jivesoftware/util/Protocol.java index f256a9728..9e1bdc652 100644 --- a/smack-extensions/src/testFixtures/java/org/jivesoftware/util/Protocol.java +++ b/smack-extensions/src/testFixtures/java/org/jivesoftware/util/Protocol.java @@ -89,6 +89,7 @@ public class Protocol { public boolean printProtocol = false; // responses to requests are taken form this queue + @SuppressWarnings("JdkObsolete") private final Queue responses = new LinkedList<>(); // list of verifications diff --git a/smack-im/build.gradle b/smack-im/build.gradle index f3cb2d475..beb30a966 100644 --- a/smack-im/build.gradle +++ b/smack-im/build.gradle @@ -1,3 +1,8 @@ +plugins { + id 'org.igniterealtime.smack.java-common-conventions' + id 'org.igniterealtime.smack.android-conventions' +} + description = """\ Smack IM. Classes and methods for XMPP-IM (RFC 6121): @@ -8,4 +13,7 @@ Roster, Chat and other functionality.""" dependencies { api project(':smack-core') testImplementation(testFixtures(project(":smack-core"))) + + // TODO: Migrate Junit4 tests to Junit5. + testImplementation "org.junit.vintage:junit-vintage-engine:$junitVersion" } diff --git a/smack-im/src/main/java/org/jivesoftware/smack/chat/Chat.java b/smack-im/src/main/java/org/jivesoftware/smack/chat/Chat.java index c57bc5e34..a9cbe98b1 100644 --- a/smack-im/src/main/java/org/jivesoftware/smack/chat/Chat.java +++ b/smack-im/src/main/java/org/jivesoftware/smack/chat/Chat.java @@ -79,7 +79,7 @@ public class Chat { /** * Returns the name of the user the chat is with. * - * @return the name of the user the chat is occuring with. + * @return the name of the user the chat is occurring with. */ public EntityJid getParticipant() { return participant; @@ -133,10 +133,12 @@ public class Chat { public void sendMessage(Message message) throws NotConnectedException, InterruptedException { // Force the recipient, message type, and thread ID since the user elected // to send the message through this chat object. - message.setTo(participant); - message.setType(Message.Type.chat); - message.setThread(threadID); - chatManager.sendMessage(this, message); + Message chatMessage = message.asBuilder() + .to(participant) + .ofType(Message.Type.chat) + .setThread(threadID) + .build(); + chatManager.sendMessage(this, chatMessage); } /** @@ -199,10 +201,10 @@ public class Chat { // Because the collector and listeners are expecting a thread ID with // a specific value, set the thread ID on the message even though it // probably never had one. - message.setThread(threadID); + Message chatMessage = message.asBuilder().setThread(threadID).build(); for (ChatMessageListener listener : listeners) { - listener.processMessage(this, message); + listener.processMessage(this, chatMessage); } } diff --git a/smack-im/src/main/java/org/jivesoftware/smack/roster/Roster.java b/smack-im/src/main/java/org/jivesoftware/smack/roster/Roster.java index 8df18a77b..b51542015 100644 --- a/smack-im/src/main/java/org/jivesoftware/smack/roster/Roster.java +++ b/smack-im/src/main/java/org/jivesoftware/smack/roster/Roster.java @@ -1,6 +1,6 @@ /** * - * Copyright 2003-2007 Jive Software, 2016-2022 Florian Schmaus. + * Copyright 2003-2007 Jive Software, 2016-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. @@ -118,8 +118,8 @@ import org.jxmpp.util.cache.LruCache; * Every entry in the roster has presence associated with it. The * {@link #getPresence(BareJid)} method will return a Presence object with the * user's presence or `null` if the user is not online or you are not subscribed - * to the user's presence. _Note:_ Presence subscription is nnot tied to the - * user being on the roster, and vice versa: You could be subscriped to a remote + * to the user's presence. _Note:_ Presence subscription is not tied to the + * user being on the roster, and vice versa: You could be subscribed to a remote * users presence without the user in your roster, and a remote user can be in * your roster without any presence subscription relation. *

    @@ -218,7 +218,7 @@ public final class Roster extends Manager { *

    * This method will never return null, instead if the user has not yet logged into * the server all modifying methods of the returned roster object - * like {@link Roster#createEntry(BareJid, String, String[])}, + * like {@link Roster#createItemAndRequestSubscription(BareJid, String, String[])}, * {@link Roster#removeEntry(RosterEntry)} , etc. except adding or removing * {@link RosterListener}s will throw an IllegalStateException. *

    @@ -242,13 +242,13 @@ public final class Roster extends Manager { private static boolean rosterLoadedAtLoginDefault = true; /** - * The default subscription processing mode to use when a Roster is created. By default + * The default subscription processing mode to use when a Roster is created. By default, * all subscription requests are automatically rejected. */ private static SubscriptionMode defaultSubscriptionMode = SubscriptionMode.reject_all; /** - * The initial maximum size of the map holding presence information of entities without an Roster entry. Currently + * The initial maximum size of the map holding presence information of entities without a Roster entry. Currently * {@value #INITIAL_DEFAULT_NON_ROSTER_PRESENCE_MAP_SIZE}. */ public static final int INITIAL_DEFAULT_NON_ROSTER_PRESENCE_MAP_SIZE = 1024; @@ -285,7 +285,7 @@ public final class Roster extends Manager { private final Map> presenceMap = new ConcurrentHashMap<>(); /** - * Like {@link presenceMap} but for presences of entities not in our Roster. + * Like {@link #presenceMap} but for presences of entities not in our Roster. */ // TODO Ideally we want here to use a LRU cache like Map which will evict all superfluous items // if their maximum size is lowered below the current item count. LruCache does not provide @@ -299,7 +299,7 @@ public final class Roster extends Manager { private final Set rosterLoadedListeners = new LinkedHashSet<>(); /** - * Mutually exclude roster listener invocation and changing the {@link entries} map. Also used + * Mutually exclude roster listener invocation and changing the {@link #entries} map. Also used * to synchronize access to either the roster listeners or the entries map. */ private final Object rosterListenersAndEntriesLock = new Object(); @@ -439,7 +439,7 @@ public final class Roster extends Manager { return; } - // Ensure that all available presences received so far in a eventually existing previous session are + // Ensure that all available presences received so far in an eventually existing previous session are // marked 'offline'. setOfflinePresencesAndResetLoaded(); @@ -754,27 +754,6 @@ public final class Roster extends Manager { return group; } - /** - * Creates a new roster entry and presence subscription. The server will asynchronously - * update the roster with the subscription status. - * - * @param user the user. (e.g. johndoe@jabber.org) - * @param name the nickname of the user. - * @param groups the list of group names the entry will belong to, or null if the - * the roster entry won't belong to a group. - * @throws NoResponseException if there was no response from the server. - * @throws XMPPErrorException if an XMPP exception occurs. - * @throws NotLoggedInException If not logged in. - * @throws NotConnectedException if the XMPP connection is not connected. - * @throws InterruptedException if the calling thread was interrupted. - * @deprecated use {@link #createItemAndRequestSubscription(BareJid, String, String[])} instead. - */ - // TODO: Remove in Smack 4.5. - @Deprecated - public void createEntry(BareJid user, String name, String[] groups) throws NotLoggedInException, NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { - createItemAndRequestSubscription(user, name, groups); - } - /** * Creates a new roster item. The server will asynchronously update the roster with the subscription status. *

    @@ -818,7 +797,7 @@ public final class Roster extends Manager { * * @param jid the XMPP address of the contact (e.g. johndoe@jabber.org) * @param name the nickname of the user. - * @param groups the list of group names the entry will belong to, or null if the + * @param groups the list of group names the entry will belong to, or null if * the roster entry won't belong to a group. * @throws NoResponseException if there was no response from the server. * @throws XMPPErrorException if an XMPP exception occurs. @@ -839,7 +818,7 @@ public final class Roster extends Manager { * * @param user the user. (e.g. johndoe@jabber.org) * @param name the nickname of the user. - * @param groups the list of group names the entry will belong to, or null if the + * @param groups the list of group names the entry will belong to, or null if * the roster entry won't belong to a group. * @throws NoResponseException if there was no response from the server. * @throws XMPPErrorException if an XMPP exception occurs. @@ -1042,7 +1021,7 @@ public final class Roster extends Manager { * Returns the roster entry associated with the given XMPP address or * null if the user is not an entry in the roster. * - * @param jid the XMPP address of the user (eg "jsmith@example.com"). The address could be + * @param jid the XMPP address of the user (e.g."jsmith@example.com"). The address could be * in any valid format (e.g. "domain/resource", "user@domain" or "user@domain/resource"). * @return the roster entry or null if it does not exist. */ @@ -1056,7 +1035,7 @@ public final class Roster extends Manager { /** * Returns true if the specified XMPP address is an entry in the roster. * - * @param jid the XMPP address of the user (eg "jsmith@example.com"). The + * @param jid the XMPP address of the user (e.g."jsmith@example.com"). The * address must be a bare JID e.g. "domain/resource" or * "user@domain". * @return true if the XMPP address is an entry in the roster. @@ -1118,11 +1097,11 @@ public final class Roster extends Manager { * {@link RosterListener}. *

    * - * @param jid the XMPP address of the user (eg "jsmith@example.com"). The + * @param jid the XMPP address of the user (e.g."jsmith@example.com"). The * address must be a bare JID e.g. "domain/resource" or * "user@domain". * @return the user's current presence, or unavailable presence if the user is offline - * or if no presence information is available.. + * or if no presence information is available. */ public Presence getPresence(BareJid jid) { Map userPresences = getPresencesInternal(jid); @@ -1652,11 +1631,20 @@ public final class Roster extends Manager { } } + + final Jid from = packet.getFrom(); + if (!isLoaded() && rosterLoadedAtLogin) { - LOGGER.warning("Roster not loaded while processing " + packet); + XMPPConnection connection = connection(); + + // Only log the warning, if this is not the reflected self-presence. Otherwise, + // the reflected self-presence may cause a spurious warning in case the + // connection got quickly shut down. See SMACK-941. + if (connection != null && from != null && !from.equals(connection.getUser())) { + LOGGER.warning("Roster not loaded while processing " + packet); + } } final Presence presence = (Presence) packet; - final Jid from = presence.getFrom(); final BareJid key; if (from != null) { diff --git a/smack-im/src/main/java/org/jivesoftware/smack/roster/RosterEntry.java b/smack-im/src/main/java/org/jivesoftware/smack/roster/RosterEntry.java index 8a13fda90..0360f892b 100644 --- a/smack-im/src/main/java/org/jivesoftware/smack/roster/RosterEntry.java +++ b/smack-im/src/main/java/org/jivesoftware/smack/roster/RosterEntry.java @@ -108,7 +108,7 @@ public final class RosterEntry extends Manager { packet.setType(IQ.Type.set); // Create a new roster item with the current RosterEntry and the *new* name. Note that we can't set the name of - // RosterEntry right away, as otherwise the updated event wont get fired, because equalsDeep would return true. + // RosterEntry right away, as otherwise the updated event won't get fired, because equalsDeep would return true. packet.addRosterItem(toRosterItem(this, name)); connection().sendIqRequestAndWaitForResponse(packet); @@ -136,7 +136,7 @@ public final class RosterEntry extends Manager { } /** - * Returns an copied list of the roster groups that this entry belongs to. + * Returns a copied list of the roster groups that this entry belongs to. * * @return an iterator for the groups this entry belongs to. */ @@ -306,7 +306,7 @@ public final class RosterEntry extends Manager { * * @param entry the roster entry. * @param name the name of the roster item. - * @param includeAskAttribute whether or not to include the 'ask' attribute. + * @param includeAskAttribute whether to include the 'ask' attribute. * @return the roster item. */ private static RosterPacket.Item toRosterItem(RosterEntry entry, String name, boolean includeAskAttribute) { diff --git a/smack-im/src/main/java/org/jivesoftware/smack/roster/RosterGroup.java b/smack-im/src/main/java/org/jivesoftware/smack/roster/RosterGroup.java index f8718fcbb..d6467dd83 100644 --- a/smack-im/src/main/java/org/jivesoftware/smack/roster/RosterGroup.java +++ b/smack-im/src/main/java/org/jivesoftware/smack/roster/RosterGroup.java @@ -116,7 +116,7 @@ public class RosterGroup extends Manager { * Returns the roster entry associated with the given XMPP address or * null if the user is not an entry in the group. * - * @param user the XMPP address of the user (eg "jsmith@example.com"). + * @param user the XMPP address of the user (e.g."jsmith@example.com"). * @return the roster entry or null if it does not exist in the group. */ public RosterEntry getEntry(Jid user) { diff --git a/smack-im/src/main/java/org/jivesoftware/smack/roster/RosterUtil.java b/smack-im/src/main/java/org/jivesoftware/smack/roster/RosterUtil.java index 843b24bad..5f2093404 100644 --- a/smack-im/src/main/java/org/jivesoftware/smack/roster/RosterUtil.java +++ b/smack-im/src/main/java/org/jivesoftware/smack/roster/RosterUtil.java @@ -1,6 +1,6 @@ /** * - * Copyright 2016 Florian Schmaus + * Copyright 2016-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. @@ -34,6 +34,7 @@ import org.jxmpp.jid.Jid; public class RosterUtil { + @SuppressWarnings("JavaUtilDate") public static void waitUntilOtherEntityIsSubscribed(Roster roster, BareJid otherEntity, long timeoutMillis) throws InterruptedException, TimeoutException { Date deadline = new Date(System.currentTimeMillis() + timeoutMillis); @@ -147,6 +148,7 @@ public class RosterUtil { ensureSubscribedTo(connectionTwo, connectionOne, timeout); } + @SuppressWarnings("JavaUtilDate") public static void ensureSubscribedTo(XMPPConnection connectionOne, XMPPConnection connectionTwo, long timeout) throws NotLoggedInException, NotConnectedException, InterruptedException, TimeoutException { Date deadline = new Date(System.currentTimeMillis() + timeout); diff --git a/smack-integration-test/build.gradle b/smack-integration-test/build.gradle index ea08655af..ede353266 100644 --- a/smack-integration-test/build.gradle +++ b/smack-integration-test/build.gradle @@ -1,13 +1,18 @@ -apply plugin: 'application' +plugins { + id 'org.igniterealtime.smack.java-common-conventions' + id 'org.igniterealtime.smack.application-conventions' +} description = """\ Smack integration tests.""" -mainClassName = 'org.igniterealtime.smack.inttest.SmackIntegrationTestFramework' -applicationDefaultJvmArgs = ["-enableassertions"] + +application { + mainClass = 'org.igniterealtime.smack.inttest.SmackIntegrationTestFramework' +} dependencies { - api project(':smack-java8-full') + api project(':smack-java11-full') api project(':smack-resolver-dnsjava') implementation project(':smack-websocket-java11') implementation "com.google.guava:guava:${guavaVersion}" @@ -20,8 +25,3 @@ dependencies { testFixturesApi(testFixtures(project(":smack-core"))) testImplementation "org.jxmpp:jxmpp-jid:$jxmppVersion:tests" } - -run { - // Pass all system properties down to the "application" run - systemProperties System.getProperties() -} 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..a0f658977 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 @@ -100,8 +100,8 @@ public class XmppConnectionStressTest { for (int c = 0; c < payloadChunkCount; c++) { int payloadChunkSize = random.nextInt(configuration.maxPayloadChunkSize) + 1; - String payloadCunk = StringUtils.randomString(payloadChunkSize, random); - JivePropertiesManager.addProperty(messageBuilder, "payload-chunk-" + c, payloadCunk); + String payloadChunk = StringUtils.randomString(payloadChunkSize, random); + JivePropertiesManager.addProperty(messageBuilder, "payload-chunk-" + c, payloadChunk); } JivePropertiesManager.addProperty(messageBuilder, MESSAGE_NUMBER_PROPERTY, i); @@ -184,7 +184,7 @@ public class XmppConnectionStressTest { Exception exception = new Exception(exceptionMessage.toString()); receiveExceptions.put(connection, exception); // TODO: Current Smack design does not guarantee that the listener won't be invoked again. - // This is because the decission to invoke a sync listeners is done at a different place + // This is because the decision to invoke a sync listeners is done at a different place // then invoking the listener. connection.removeSyncStanzaListener(this); receivedSemaphore.release(); @@ -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++) { @@ -337,7 +338,7 @@ public class XmppConnectionStressTest { sb.append("Exceptions while sending and/or receiving."); if (!sendExceptions.isEmpty()) { - sb.append(" Send exxceptions: "); + sb.append(" Send exceptions: "); for (Map.Entry entry : sendExceptions.entrySet()) { sb.append(entry.getKey()).append(": ").append(entry.getValue()).append(';'); } diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackIntTest.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackIntTest.java index 2b94b2190..9f94fe26b 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackIntTest.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackIntTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2020 Florian Schmaus + * Copyright 2015-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. @@ -20,6 +20,7 @@ import java.io.File; import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; +import java.util.List; import java.util.Random; import java.util.concurrent.TimeoutException; import java.util.logging.Logger; @@ -33,6 +34,11 @@ import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPException.XMPPErrorException; import org.jivesoftware.smack.filter.StanzaFilter; +import org.igniterealtime.smack.inttest.util.MultiResultSyncPoint; +import org.igniterealtime.smack.inttest.util.ResultSyncPoint; + +import org.opentest4j.AssertionFailedError; + public abstract class AbstractSmackIntTest { protected static final Logger LOGGER = Logger.getLogger(AbstractSmackIntTest.class.getName()); @@ -90,4 +96,32 @@ public abstract class AbstractSmackIntTest { } return urlConnection; } + + public R assertResult(ResultSyncPoint syncPoint, String message) throws InterruptedException, TimeoutException, AssertionFailedError { + return assertResult(syncPoint, timeout, message); + } + + public static R assertResult(ResultSyncPoint syncPoint, long timeout, String message) throws InterruptedException, TimeoutException, AssertionFailedError { + try { + return syncPoint.waitForResult(timeout, message); + } catch (InterruptedException | TimeoutException e) { + throw e; + } catch (Exception e) { + throw new AssertionFailedError(message, e); + } + } + + public List assertResult(MultiResultSyncPoint syncPoint, String message) throws InterruptedException, TimeoutException, AssertionFailedError { + return assertResult(syncPoint, timeout, message); + } + + public static List assertResult(MultiResultSyncPoint syncPoint, long timeout, String message) throws InterruptedException, TimeoutException, AssertionFailedError { + try { + return syncPoint.waitForResults(timeout, message); + } catch (InterruptedException | TimeoutException e) { + throw e; + } catch (Exception e) { + throw new AssertionFailedError(message, e); + } + } } diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackLowLevelIntegrationTest.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackLowLevelIntegrationTest.java index d517494db..480129575 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackLowLevelIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackLowLevelIntegrationTest.java @@ -50,7 +50,7 @@ public abstract class AbstractSmackLowLevelIntegrationTest extends AbstractSmack * Get a connected connection. Note that this method will return a connection constructed via the default connection * descriptor. It is primarily meant for integration tests to discover if the XMPP service supports a certain * feature, that the integration test requires to run. This feature discovery is typically done in the constructor of the - * integration tests. And if the discovery fails a {@link TestNotPossibleException} should be thrown by he constructor. + * integration tests. And if the discovery fails a {@link TestNotPossibleException} should be thrown by the constructor. * *

    Please ensure that you invoke {@link #recycle(AbstractXMPPConnection connection)} once you are done with this connection. * 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..fab1e00c9 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-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. @@ -22,20 +22,24 @@ import java.io.IOException; import java.lang.reflect.Method; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.logging.Logger; +import java.util.stream.Collectors; import javax.net.ssl.SSLContext; import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode; import org.jivesoftware.smack.debugger.ConsoleDebugger; +import org.jivesoftware.smack.debugger.SmackDebuggerFactory; import org.jivesoftware.smack.util.CollectionUtil; import org.jivesoftware.smack.util.Function; import org.jivesoftware.smack.util.Objects; @@ -61,12 +65,6 @@ public final class Configuration { serviceAdministration, } - public enum Debugger { - none, - console, - enhanced, - } - public enum DnsResolver { minidns, javax, @@ -75,6 +73,8 @@ public final class Configuration { public final DomainBareJid service; + public final String host; + public final String serviceTlsPin; public final SslContextFactory sslContextFactory; @@ -101,7 +101,7 @@ public final class Configuration { public final String accountThreePassword; - public final Debugger debugger; + public final SmackDebuggerFactory debuggerFactory; public final Set enabledTests; @@ -111,6 +111,10 @@ public final class Configuration { private final Map> disabledTestsMap; + public final Set enabledSpecifications; + + public final Set disabledSpecifications; + public final String defaultConnectionNickname; public final Set enabledConnections; @@ -132,9 +136,12 @@ public final class Configuration { public final CompatibilityMode compatibilityMode; + public final List testRunResultProcessors; + private Configuration(Configuration.Builder builder) throws KeyManagementException, NoSuchAlgorithmException { service = Objects.requireNonNull(builder.service, "'service' must be set. Either via 'properties' files or via system property 'sinttest.service'."); + host = builder.host; serviceTlsPin = builder.serviceTlsPin; if (serviceTlsPin != null) { SSLContext sslContext = Java7Pinning.forPin(serviceTlsPin); @@ -146,9 +153,9 @@ public final class Configuration { if (builder.replyTimeout > 0) { replyTimeout = builder.replyTimeout; } else { - replyTimeout = 60000; + replyTimeout = 47000; } - debugger = builder.debugger; + debuggerFactory = builder.debuggerFactory; if (StringUtils.isNotEmpty(builder.adminAccountUsername, builder.adminAccountPassword)) { accountRegistration = AccountRegistration.serviceAdministration; } @@ -181,6 +188,8 @@ public final class Configuration { this.enabledTestsMap = convertTestsToMap(enabledTests); this.disabledTests = CollectionUtil.nullSafeUnmodifiableSet(builder.disabledTests); this.disabledTestsMap = convertTestsToMap(disabledTests); + this.enabledSpecifications = CollectionUtil.nullSafeUnmodifiableSet(builder.enabledSpecifications); + this.disabledSpecifications = CollectionUtil.nullSafeUnmodifiableSet(builder.disabledSpecifications); this.defaultConnectionNickname = builder.defaultConnectionNickname; this.enabledConnections = builder.enabledConnections; this.disabledConnections = builder.disabledConnections; @@ -192,17 +201,12 @@ public final class Configuration { } b.setSecurityMode(securityMode); b.setXmppDomain(service); + if (host != null) { + b.setHost(host); + } - switch (debugger) { - case enhanced: - b.setDebuggerFactory(EnhancedDebugger.Factory.INSTANCE); - break; - case console: - b.setDebuggerFactory(ConsoleDebugger.Factory.INSTANCE); - break; - case none: - // Nothing to do :). - break; + if (debuggerFactory != null) { + b.setDebuggerFactory(debuggerFactory); } }; @@ -210,6 +214,7 @@ public final class Configuration { this.dnsResolver = builder.dnsResolver; this.compatibilityMode = builder.compatibilityMode; + this.testRunResultProcessors = builder.testRunResultProcessors; } public boolean isAccountRegistrationPossible() { @@ -224,6 +229,8 @@ public final class Configuration { private DomainBareJid service; + private String host; + private String serviceTlsPin; private SecurityMode securityMode; @@ -246,12 +253,16 @@ public final class Configuration { public String accountThreePassword; - private Debugger debugger = Debugger.none; + private SmackDebuggerFactory debuggerFactory; private Set enabledTests; private Set disabledTests; + private Set enabledSpecifications; + + private Set disabledSpecifications; + private String defaultConnectionNickname; private Set enabledConnections; @@ -266,6 +277,8 @@ public final class Configuration { private CompatibilityMode compatibilityMode = CompatibilityMode.standardsCompliant; + private List testRunResultProcessors; + private Builder() { } @@ -283,6 +296,11 @@ public final class Configuration { return this; } + private Builder setHost(String host) { + this.host = host; + return this; + } + public Builder addEnabledTest(Class enabledTest) { if (enabledTests == null) { enabledTests = new HashSet<>(); @@ -315,7 +333,7 @@ public final class Configuration { this.accountOneUsername = StringUtils.requireNotNullNorEmpty(accountOneUsername, "accountOneUsername must not be null nor empty"); this.accountOnePassword = StringUtils.requireNotNullNorEmpty(accountOnePassword, "accountOnePassword must not be null nor empty"); this.accountTwoUsername = StringUtils.requireNotNullNorEmpty(accountTwoUsername, "accountTwoUsername must not be null nor empty"); - this.accountTwoPassword = StringUtils.requireNotNullNorEmpty(accountTwoPassword, "accountTwoPasswordmust not be null nor empty"); + this.accountTwoPassword = StringUtils.requireNotNullNorEmpty(accountTwoPassword, "accountTwoPassword must not be null nor empty"); this.accountThreeUsername = StringUtils.requireNotNullNorEmpty(accountThreeUsername, "accountThreeUsername must not be null nor empty"); this.accountThreePassword = StringUtils.requireNotNullNorEmpty(accountThreePassword, "accountThreePassword must not be null nor empty"); return this; @@ -352,18 +370,23 @@ public final class Configuration { case "false": // For backwards compatibility settings with previous boolean setting. LOGGER.warning("Debug string \"" + debuggerString + "\" is deprecated, please use \"none\" instead"); case "none": - debugger = Debugger.none; + debuggerFactory = null; break; case "true": // For backwards compatibility settings with previous boolean setting. LOGGER.warning("Debug string \"" + debuggerString + "\" is deprecated, please use \"console\" instead"); case "console": - debugger = Debugger.console; + debuggerFactory = ConsoleDebugger.Factory.INSTANCE; break; case "enhanced": - debugger = Debugger.enhanced; + debuggerFactory = EnhancedDebugger.Factory.INSTANCE; break; default: - throw new IllegalArgumentException("Unrecognized debugger string: " + debuggerString); + try { + final Class aClass = Class.forName(debuggerString).asSubclass(SmackDebuggerFactory.class); + debuggerFactory = aClass.getConstructor().newInstance(); + } catch (Exception e) { + throw new IllegalArgumentException("Unable to construct debugger from value: " + debuggerString, e); + } } return this; } @@ -378,6 +401,16 @@ public final class Configuration { return this; } + public Builder setEnabledSpecifications(String enabledSpecificationsString) { + enabledSpecifications = getSpecificationSetFrom(enabledSpecificationsString); + return this; + } + + public Builder setDisabledSpecifications(String disabledSpecificationsString) { + disabledSpecifications = getSpecificationSetFrom(disabledSpecificationsString); + return this; + } + public Builder setDefaultConnection(String defaultConnectionNickname) { this.defaultConnectionNickname = defaultConnectionNickname; return this; @@ -461,6 +494,15 @@ public final class Configuration { return setCompatibilityMode(compatibilityMode); } + public Builder setTestRunResultProcessors(String testRunResultProcessorsString) { + if (testRunResultProcessorsString == null) { + return this; + } + + testRunResultProcessors = getTestRunProcessorListFrom(testRunResultProcessorsString); + return this; + } + public Configuration build() throws KeyManagementException, NoSuchAlgorithmException { return new Configuration(this); } @@ -488,14 +530,15 @@ 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.setHost(properties.getProperty("host")); 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"); @@ -517,12 +560,14 @@ public final class Configuration { String debugString = properties.getProperty("debug"); if (debugString != null) { - LOGGER.warning("Usage of depreacted 'debug' option detected, please use 'debugger' instead"); + LOGGER.warning("Usage of deprecated 'debug' option detected, please use 'debugger' instead"); builder.setDebugger(debugString); } builder.setDebugger(properties.getProperty("debugger")); builder.setEnabledTests(properties.getProperty("enabledTests")); builder.setDisabledTests(properties.getProperty("disabledTests")); + builder.setEnabledSpecifications(properties.getProperty("enabledSpecifications")); + builder.setDisabledSpecifications(properties.getProperty("disabledSpecifications")); builder.setDefaultConnection(properties.getProperty("defaultConnection")); builder.setEnabledConnections(properties.getProperty("enabledConnections")); builder.setDisabledConnections(properties.getProperty("disabledConnections")); @@ -536,11 +581,14 @@ public final class Configuration { builder.setCompatibilityMode(properties.getProperty("compatibilityMode")); + builder.setTestRunResultProcessors(properties.getProperty("testRunResultProcessors", + SmackIntegrationTestFramework.JulTestRunResultProcessor.class.getName())); + return builder.build(); } private static File findPropertiesFile() { - List possibleLocations = new LinkedList<>(); + List possibleLocations = new ArrayList<>(); possibleLocations.add("properties"); String userHome = System.getProperty("user.home"); if (userHome != null) { @@ -587,6 +635,23 @@ public final class Configuration { }); } + private static Set getSpecificationSetFrom(String input) { + return split(input, Configuration::normalizeSpecification); + } + + private static List getTestRunProcessorListFrom(String input) { + return Arrays.stream(input.split(",")) + .map(element -> { + try { + final Class aClass = Class.forName(element).asSubclass(SmackIntegrationTestFramework.TestRunResultProcessor.class); + return aClass.getConstructor().newInstance(); + } catch (Exception e) { + throw new IllegalArgumentException("Unable to construct TestRunResultProcessor from value: " + element, e); + } + }) + .collect(Collectors.toList()); + } + private static Map> convertTestsToMap(Set tests) { Map> res = new HashMap<>(); for (String test : tests) { @@ -695,4 +760,34 @@ public final class Configuration { return contains(method, disabledTestsMap); } + public boolean isSpecificationEnabled(String specification) { + if (enabledSpecifications.isEmpty()) { + return true; + } + + if (specification == null) { + return false; + } + + return enabledSpecifications.contains(normalizeSpecification(specification)); + } + + public boolean isSpecificationDisabled(String specification) { + if (disabledSpecifications.isEmpty()) { + return false; + } + + if (specification == null) { + return false; + } + + return disabledSpecifications.contains(normalizeSpecification(specification)); + } + + public static String normalizeSpecification(String specification) { + if (specification == null || specification.isBlank()) { + return null; + } + return specification.replaceAll("[\\s-]", "").toUpperCase(Locale.ROOT); + } } 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..3fb4edaae 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 @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2023 Florian Schmaus + * Copyright 2015-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. @@ -39,11 +39,12 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; -import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; import java.util.logging.Level; import java.util.logging.Logger; @@ -64,6 +65,7 @@ import org.jivesoftware.smack.util.dns.dnsjava.DNSJavaResolver; import org.jivesoftware.smack.util.dns.javax.JavaxResolver; import org.jivesoftware.smack.util.dns.minidns.MiniDnsResolver; +import org.jivesoftware.smackx.debugger.EnhancedDebugger; import org.jivesoftware.smackx.debugger.EnhancedDebuggerWindow; import org.jivesoftware.smackx.iqregister.AccountManager; @@ -71,6 +73,7 @@ import org.igniterealtime.smack.inttest.Configuration.AccountRegistration; import org.igniterealtime.smack.inttest.annotations.AfterClass; import org.igniterealtime.smack.inttest.annotations.BeforeClass; import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; +import org.igniterealtime.smack.inttest.annotations.SpecificationReference; import org.reflections.Reflections; import org.reflections.scanners.MethodAnnotationsScanner; import org.reflections.scanners.MethodParameterScanner; @@ -87,6 +90,8 @@ public class SmackIntegrationTestFramework { public static boolean SINTTEST_UNIT_TEST = false; + private static ConcreteTest TEST_UNDER_EXECUTION; + protected final Configuration config; protected TestRunResult testRunResult; @@ -108,47 +113,86 @@ public class SmackIntegrationTestFramework { SmackIntegrationTestFramework sinttest = new SmackIntegrationTestFramework(config); TestRunResult testRunResult = sinttest.run(); - for (Map.Entry, Throwable> entry : testRunResult.impossibleTestClasses.entrySet()) { - LOGGER.info("Could not run " + entry.getKey().getName() + " because: " - + entry.getValue().getLocalizedMessage()); - } - for (TestNotPossible testNotPossible : testRunResult.impossibleIntegrationTests) { - LOGGER.info("Could not run " + testNotPossible.concreteTest + " because: " - + testNotPossible.testNotPossibleException.getMessage()); - } - for (SuccessfulTest successfulTest : testRunResult.successfulIntegrationTests) { - LOGGER.info(successfulTest.concreteTest + " ✔"); - } - final int successfulTests = testRunResult.successfulIntegrationTests.size(); - final int failedTests = testRunResult.failedIntegrationTests.size(); - final int availableTests = testRunResult.getNumberOfAvailableTests(); - LOGGER.info("SmackIntegrationTestFramework[" + testRunResult.testRunId + ']' + " finished: " - + successfulTests + '/' + availableTests + " [" + failedTests + " failed]"); - - final int exitStatus; - if (failedTests > 0) { - LOGGER.warning("💀 The following " + failedTests + " tests failed! 💀"); - for (FailedTest failedTest : testRunResult.failedIntegrationTests) { - final Throwable cause = failedTest.failureReason; - LOGGER.log(Level.SEVERE, failedTest.concreteTest + " failed: " + cause, cause); + for (final TestRunResultProcessor testRunResultProcessor : config.testRunResultProcessors) { + try { + testRunResultProcessor.process(testRunResult); + } catch (Throwable t) { + LOGGER.log(Level.WARNING, "Invocation of TestRunResultProcessor " + testRunResultProcessor + " failed.", t); } - exitStatus = 2; - } else { - LOGGER.info("All possible Smack Integration Tests completed successfully. \\o/"); - exitStatus = 0; } - switch (config.debugger) { - case enhanced: + if (config.debuggerFactory instanceof EnhancedDebugger) { EnhancedDebuggerWindow.getInstance().waitUntilClosed(); - break; - default: - break; } + final int exitStatus = testRunResult.failedIntegrationTests.isEmpty() ? 0 : 2; System.exit(exitStatus); } + public static class JulTestRunResultProcessor implements TestRunResultProcessor { + + @Override + public void process(final TestRunResult testRunResult) { + for (Map.Entry, Throwable> entry : testRunResult.impossibleTestClasses.entrySet()) { + LOGGER.info("Could not run " + entry.getKey().getName() + " because: " + + entry.getValue().getLocalizedMessage()); + } + for (TestNotPossible testNotPossible : testRunResult.impossibleIntegrationTests) { + LOGGER.info("Could not run " + testNotPossible.concreteTest + " because: " + + testNotPossible.testNotPossibleException.getMessage()); + } + for (SuccessfulTest successfulTest : testRunResult.successfulIntegrationTests) { + LOGGER.info(successfulTest.concreteTest + " ✔"); + } + final int successfulTests = testRunResult.successfulIntegrationTests.size(); + final int failedTests = testRunResult.failedIntegrationTests.size(); + final int availableTests = testRunResult.getNumberOfAvailableTests(); + LOGGER.info("SmackIntegrationTestFramework[" + testRunResult.testRunId + ']' + " finished: " + + successfulTests + '/' + availableTests + " [" + failedTests + " failed]"); + + if (failedTests > 0) { + LOGGER.warning("💀 The following " + failedTests + " tests failed! 💀"); + final SortedSet bySpecification = new TreeSet<>(); + for (FailedTest failedTest : testRunResult.failedIntegrationTests) { + final Throwable cause = failedTest.failureReason; + LOGGER.log(Level.SEVERE, failedTest.concreteTest + " failed: " + cause, cause); + if (failedTest.concreteTest.method.getDeclaringClass().isAnnotationPresent(SpecificationReference.class)) { + final String specificationReference = getSpecificationReference(failedTest.concreteTest.method); + if (specificationReference != null) { + bySpecification.add("- " + specificationReference + " (as tested by '" + failedTest.concreteTest + "')"); + } + } + } + if (!bySpecification.isEmpty()) { + LOGGER.log(Level.SEVERE, "The failed tests correspond to the following specifications:" + System.lineSeparator() + String.join(System.lineSeparator(), bySpecification)); + } + } else { + LOGGER.info("All possible Smack Integration Tests completed successfully. \\o/"); + } + } + } + + private static String getSpecificationReference(Method method) { + final SpecificationReference spec = method.getDeclaringClass().getAnnotation(SpecificationReference.class); + if (spec == null || spec.document().isBlank()) { + return null; + } + String line = spec.document().trim(); + if (!spec.version().isBlank()) { + line += " (version " + spec.version() + ")"; + } + + final SmackIntegrationTest test = method.getAnnotation(SmackIntegrationTest.class); + if (!test.section().isBlank()) { + line += " section " + test.section().trim(); + } + if (!test.quote().isBlank()) { + line += ":\t\"" + test.quote().trim() + "\""; + } + assert !line.isBlank(); + return line; + } + public SmackIntegrationTestFramework(Configuration configuration) { this.config = configuration; } @@ -175,7 +219,7 @@ public class SmackIntegrationTestFramework { this.connectionManager = new XmppConnectionManager(this); LOGGER.info("SmackIntegrationTestFramework [" + testRunResult.testRunId + ']' + ": Starting\nSmack version: " + Smack.getVersion()); - if (config.debugger != Configuration.Debugger.none) { + if (config.debuggerFactory != null) { // JUL Debugger will not print any information until configured to print log messages of // level FINE // TODO configure JUL for log? @@ -192,7 +236,7 @@ public class SmackIntegrationTestFramework { String[] testPackages; if (config.testPackages == null || config.testPackages.isEmpty()) { - testPackages = new String[] { "org.jivesoftware.smackx", "org.jivesoftware.smack" }; + testPackages = new String[] { "org.jivesoftware.smackx", "org.jivesoftware.smack", "org.igniterealtime.smackx", "org.igniterealtime.smack" }; } else { testPackages = config.testPackages.toArray(new String[config.testPackages.size()]); @@ -224,7 +268,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 { @@ -243,6 +288,10 @@ public class SmackIntegrationTestFramework { return testRunResult; } + public static ConcreteTest getTestUnderExecution() { + return TEST_UNDER_EXECUTION; + } + @SuppressWarnings({"Finally"}) private void runTests(Set> classes) throws InterruptedException, InstantiationException, IllegalAccessException, @@ -291,7 +340,27 @@ public class SmackIntegrationTestFramework { } if (config.isClassDisabled(testClass)) { - DisabledTestClass disabledTestClass = new DisabledTestClass(testClass, "Skipping test class " + testClassName + " because it is disalbed"); + DisabledTestClass disabledTestClass = new DisabledTestClass(testClass, "Skipping test class " + testClassName + " because it is disabled"); + testRunResult.disabledTestClasses.add(disabledTestClass); + continue; + } + + final String specification; + if (testClass.isAnnotationPresent(SpecificationReference.class)) { + final SpecificationReference specificationReferenceAnnotation = testClass.getAnnotation(SpecificationReference.class); + specification = Configuration.normalizeSpecification(specificationReferenceAnnotation.document()); + } else { + specification = null; + } + + if (!config.isSpecificationEnabled(specification)) { + DisabledTestClass disabledTestClass = new DisabledTestClass(testClass, "Skipping test method " + testClass + " because it tests a specification ('" + specification + "') that is not enabled"); + testRunResult.disabledTestClasses.add(disabledTestClass); + continue; + } + + if (config.isSpecificationDisabled(specification)) { + DisabledTestClass disabledTestClass = new DisabledTestClass(testClass, "Skipping test method " + testClass + " because it tests a specification ('" + specification + "') that is disabled"); testRunResult.disabledTestClasses.add(disabledTestClass); continue; } @@ -362,7 +431,7 @@ public class SmackIntegrationTestFramework { final Class[] parameterTypes = method.getParameterTypes(); if (parameterTypes.length > 0) { throw new IllegalStateException( - "SmackIntegrationTest annotaton on " + method + " that takes arguments "); + "SmackIntegrationTest annotation on " + method + " that takes arguments "); } break; case LowLevel: @@ -457,6 +526,11 @@ public class SmackIntegrationTestFramework { } sb.append('\n'); } + + if (numberOfAvailableTests == 0) { + throw new IllegalArgumentException("No integration tests selected."); + } + sb.append("Available tests: ").append(numberOfAvailableTests); if (!testRunResult.disabledTestClasses.isEmpty() || !testRunResult.disabledTests.isEmpty()) { sb.append(" (Disabled ").append(testRunResult.disabledTestClasses.size()).append(" classes") @@ -493,7 +567,7 @@ public class SmackIntegrationTestFramework { return; } Throwable nonFatalFailureReason; - // junit assert's throw an AssertionError if they fail, those should not be + // junit asserts throw an AssertionError if they fail, those should not be // thrown up, as it would be done by throwFatalException() if (cause instanceof AssertionError) { nonFatalFailureReason = cause; @@ -593,8 +667,8 @@ public class SmackIntegrationTestFramework { throw (InterruptedException) e; } - // We handle NullPointerException as a non fatal exception, as they are mostly caused by an invalid reply where - // a extension element is missing. Consider for example + // We handle NullPointerException as a non-fatal exception, as they are mostly caused by an invalid reply where + // an extension element is missing. Consider for example // assertEquals(StanzaError.Condition.foo, response.getError().getCondition()) // Otherwise NPEs could be caused by an internal bug in Smack, e.g. missing null handling. if (e instanceof NullPointerException) { @@ -609,18 +683,23 @@ public class SmackIntegrationTestFramework { return (Exception) e; } + @FunctionalInterface + public interface TestRunResultProcessor { + void process(SmackIntegrationTestFramework.TestRunResult testRunResult); + } + public static final class TestRunResult { /** - * A short String of lowercase characters and numbers used to identify a integration test + * A short String of lowercase characters and numbers used to identify an integration test * run. We use lowercase characters because this string will eventually be part of the - * localpart of the used JIDs (and the localpart is case insensitive). + * localpart of the used JIDs (and the localpart is case-insensitive). */ public final String testRunId = StringUtils.insecureRandomString(5).toLowerCase(Locale.US); - private final List successfulIntegrationTests = Collections.synchronizedList(new LinkedList()); - private final List failedIntegrationTests = Collections.synchronizedList(new LinkedList()); - private final List impossibleIntegrationTests = Collections.synchronizedList(new LinkedList()); + private final List successfulIntegrationTests = Collections.synchronizedList(new ArrayList()); + private final List failedIntegrationTests = Collections.synchronizedList(new ArrayList()); + private final List impossibleIntegrationTests = Collections.synchronizedList(new ArrayList()); // TODO: Ideally three would only be a list of disabledTests, but since we do not process a disabled test class // any further, we can not determine the concrete disabled tests. @@ -679,7 +758,12 @@ public class SmackIntegrationTestFramework { executeSinttestSpecialMethod(beforeClassMethod); for (ConcreteTest concreteTest : concreteTests) { - runConcreteTest(concreteTest); + TEST_UNDER_EXECUTION = concreteTest; + try { + runConcreteTest(concreteTest); + } finally { + TEST_UNDER_EXECUTION = null; + } } } finally { @@ -728,21 +812,33 @@ public class SmackIntegrationTestFramework { return null; } - static final class ConcreteTest { + public static final class ConcreteTest { private final TestType testType; private final Method method; private final Executor executor; - private final String[] subdescriptons; + private final List subdescriptons; private ConcreteTest(TestType testType, Method method, Executor executor, String... subdescriptions) { this.testType = testType; this.method = method; this.executor = executor; - this.subdescriptons = subdescriptions; + this.subdescriptons = List.of(subdescriptions); } private transient String stringCache; + public TestType getTestType() { + return testType; + } + + public Method getMethod() { + return method; + } + + public List getSubdescriptons() { + return subdescriptons; + } + @Override public String toString() { if (stringCache != null) { @@ -755,9 +851,9 @@ public class SmackIntegrationTestFramework { .append(method.getName()) .append(" (") .append(testType.name()); - if (subdescriptons != null && subdescriptons.length > 0) { + if (!subdescriptons.isEmpty()) { sb.append(", "); - StringUtils.appendTo(Arrays.asList(subdescriptons), sb); + StringUtils.appendTo(subdescriptons, sb); } sb.append(')'); diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/TestNotPossibleException.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/TestNotPossibleException.java index 843726ca8..cf7030efd 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/TestNotPossibleException.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/TestNotPossibleException.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015 Florian Schmaus + * Copyright 2015-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. @@ -26,4 +26,12 @@ public class TestNotPossibleException extends Exception { public TestNotPossibleException(String reason) { super(reason); } + + public TestNotPossibleException(Throwable reason) { + super(reason); + } + + public TestNotPossibleException(String message, Throwable reason) { + super(message, reason); + } } diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/XmppConnectionDescriptor.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/XmppConnectionDescriptor.java index 173407429..adcdcaa40 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/XmppConnectionDescriptor.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/XmppConnectionDescriptor.java @@ -155,7 +155,7 @@ public final class XmppConnectionDescriptor< return XmppConnectionDescriptor.buildWith(ModularXmppClientToServerConnection.class, ModularXmppClientToServerConnectionConfiguration.class, ModularXmppClientToServerConnectionConfiguration.Builder.class) .withNickname(nickname) - .applyExtraConfguration(cb -> { + .applyExtraConfiguration(cb -> { cb.removeAllModules(); ModularXmppClientToServerConnectionModuleDescriptor webSocketModuleDescriptor = XmppWebSocketTransportModuleDescriptor.getBuilder(cb) @@ -184,7 +184,13 @@ public final class XmppConnectionDescriptor< nickname = connectionClass.getSimpleName(); } + // TODO Remove in Smack 4.6 + @Deprecated // Replaced by applyExtraConfiguration(Consumer extraBuilder) public Builder applyExtraConfguration(Consumer extraBuilder) { + return applyExtraConfiguration(extraBuilder); + } + + public Builder applyExtraConfiguration(Consumer extraBuilder) { this.extraBuilder = extraBuilder; return this; } 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..3b2f21b60 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 @@ -85,7 +85,7 @@ public class XmppConnectionManager { addConnectionDescriptor( XmppConnectionDescriptor.buildWith(ModularXmppClientToServerConnection.class, ModularXmppClientToServerConnectionConfiguration.class, ModularXmppClientToServerConnectionConfiguration.Builder.class) .withNickname("modular-nocompress") - .applyExtraConfguration(cb -> cb.removeModule(CompressionModuleDescriptor.class)) + .applyExtraConfiguration(cb -> cb.removeModule(CompressionModuleDescriptor.class)) .build() ); addConnectionDescriptor( @@ -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/igniterealtime/smack/inttest/annotations/SmackIntegrationTest.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/annotations/SmackIntegrationTest.java index 173b4d2e9..55b1ef684 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/annotations/SmackIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/annotations/SmackIntegrationTest.java @@ -31,4 +31,18 @@ public @interface SmackIntegrationTest { int connectionCount() default -1; + /** + * Unique identifier for a section (or paragraph) of the document referenced by {@link SpecificationReference}, + * such as '6.2.1'. + * + * @return a document section identifier + */ + String section() default ""; + + /** + * A quotation of relevant text from the section referenced by {@link #section()}. + * + * @return human-readable text from the references document and section. + */ + String quote() default ""; } diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/annotations/SpecificationReference.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/annotations/SpecificationReference.java new file mode 100644 index 000000000..beadb7782 --- /dev/null +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/annotations/SpecificationReference.java @@ -0,0 +1,48 @@ +/** + * + * Copyright 2024 Guus der Kinderen + * + * 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.igniterealtime.smack.inttest.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Reference to a specification document. + * + * @author Guus der Kinderen, guus@goodbytes.nl + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface SpecificationReference { + + /** + * Unique identifier for a specification document, such as 'RFC 6120' or 'XEP-0485'. + * + * @return a document identifier + */ + String document(); + + /** + * An optional version number, such as '1.2.0'. + * + * @return a version number + */ + String version() default ""; +} diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/package-info.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/package-info.java index 2caf8d792..84301a2f5 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/package-info.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/package-info.java @@ -80,6 +80,10 @@ * XMPP service to run the tests on * * + * host + * IP address or DNS name of the XMPP service to run the tests on + * + * * serviceTlsPin * TLS Pin (used by java-pinning) * @@ -125,7 +129,7 @@ * * * debugger - * ‘console’ for console debugger, ‘enhanced’ for the enhanced debugger + * ‘console’ for console debugger, ‘enhanced’ for the enhanced debugger, or the name of a class that implements SmackDebuggerFactory for a custom debugger * * * enabledTests @@ -136,6 +140,14 @@ * List of disabled tests * * + * enabledSpecifications + * List of specifications for which to enable tests + * + * + * disabledSpecifications + * List of specifications for which to disable tests + * + * * defaultConnection * Nickname of the default connection * @@ -159,6 +171,10 @@ * dnsResolver * One of ‘minidns’, ‘javax’ or ‘dnsjava’. Defaults to ‘minidns’. * + * + * testRunResultProcessors + * List of class names for generating test run output. Defaults to 'org.igniterealtime.smack.inttest.SmackIntegrationTestFramework$ConsoleTestRunResultProcessor' + * * *

    Where to place the properties file

    *

    @@ -187,6 +203,20 @@ *

    * would run all tests defined in the SoftwareInfoIntegrationTest class. *

    + *

    + * Use enabledSpecifications to run all tests that assert implementation of functionality that is described + * in standards identified by the provided specification-reference. + *

    + *

    + * For example: + *

    + * + *
    + * $ gradle integrationTest -Dsinttest.enabledSpecifications=XEP-0045
    + * 
    + *

    + * would run all tests that are annotated to verify functionality specified in XEP-0045: "Multi-User Chat". + *

    *

    Overview of the components

    *

    * Package org.igniterealtime.smack.inttest @@ -249,7 +279,7 @@ * the required XMPP feature. If it does not, simply throw a TestNotPossibleException. *

    *

    - * Test methods must be public, take zero arguments i.e. declare no parameters and be annoated with + * Test methods must be public, take zero arguments i.e. declare no parameters and be annotated with * @SmackIntegrationTest. If the test method is not able to perform a test then it should throw a * TestNotPossibleException. *

    @@ -266,7 +296,7 @@ *

    *

    Low-Level Integration Tests

    *

    - * Classes that implement low-level integration tests need to sublcass + * Classes that implement low-level integration tests need to subclass * {@link org.igniterealtime.smack.inttest.AbstractSmackLowLevelIntegrationTest}. The test methods can declare as many * parameters as they need to, but every parameter must be of type XMPPTCPConnection. The framework will * automatically create, register and login the connections. After the test is finished, the connections will be @@ -284,6 +314,18 @@ * Debug Window launching when your tests launch, and you'll get a stanza-by-stanza account of what happened on each * connection, hopefully enough to diagnose what went wrong. *

    + *

    + * Lastly, you can provide a custom debugger, by providing the fully qualified name of a class that implements + * {@link org.jivesoftware.smack.debugger.SmackDebuggerFactory}. The provided factory must declare a public constructor + * that takes no arguments. + *

    + *

    + * Example: + *

    + * + *
    {@code
    + * $ gradle integrationTest -Dsinttest.service=my.xmppservice.org -Dsinttest.debugger="org.example.MyDebugger$Factory"
    + * }
    *

    Debugging in the IDE

    *

    * If the output isn't enough, you may need to debug and inspect running code within the IDE. Depending on the IDE, in @@ -302,7 +344,20 @@ *

    * *
    {@code
    - * $ gradle integrationTest -Dsinttest.service=my.xmppserivce.org -Dsinttest.testPackages=org.mypackage,org.otherpackage
    + * $ gradle integrationTest -Dsinttest.service=my.xmppservice.org -Dsinttest.testPackages=org.mypackage,org.otherpackage
    + * }
    + *

    Generating test run reports

    + *

    + * By default, the results of the test run is printed to standard-error. You can, however, provide your own processing + * for the test run results. To do so, create an implementation of + * {@link org.igniterealtime.smack.inttest.SmackIntegrationTestFramework.TestRunResultProcessor} and provide its class + * name to the testRunResultProcessor property. This property takes a comma separated list of class names. + *

    + *

    + * Example: + *

    + *
    {@code
    + * $ gradle integrationTest -Dsinttest.service=my.xmppservice.org -Dsinttest.testRunResultProcessor=org.igniterealtime.smack.inttest.SmackIntegrationTestFramework$ConsoleTestRunResultProcessor
      * }
    */ package org.igniterealtime.smack.inttest; diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/util/IntegrationTestRosterUtil.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/util/IntegrationTestRosterUtil.java index 4a6569dcf..befa9eb23 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/util/IntegrationTestRosterUtil.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/util/IntegrationTestRosterUtil.java @@ -79,7 +79,7 @@ public class IntegrationTestRosterUtil { try { presenceRequestingRoster.sendSubscriptionRequest(presenceRequestReceiverAddress.asBareJid()); - syncPoint.waitForResult(timeout); + syncPoint.waitForResult(timeout, "Timeout while waiting for subscription request of '" + presenceRequestingAddress + "' to '" + presenceRequestReceiverAddress + "' to be answered."); } finally { presenceRequestReceiverRoster.removeSubscribeListener(subscribeListener); presenceRequestingRoster.removePresenceEventListener(presenceEventListener); diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/util/MultiResultSyncPoint.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/util/MultiResultSyncPoint.java new file mode 100644 index 000000000..aaf0bd4f1 --- /dev/null +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/util/MultiResultSyncPoint.java @@ -0,0 +1,71 @@ +/** + * + * Copyright 2021-2024 Guus der Kinderen + * + * 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.igniterealtime.smack.inttest.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeoutException; + +import org.jivesoftware.smack.util.Objects; + +public class MultiResultSyncPoint { + + private final List results; + private E exception; + private final int expectedResultCount; + + public MultiResultSyncPoint(int expectedResultCount) { + this.expectedResultCount = expectedResultCount; + this.results = new ArrayList<>(expectedResultCount); + } + + public synchronized List waitForResults(long timeout) throws E, InterruptedException, TimeoutException { + return waitForResults(timeout, null); + } + + public synchronized List waitForResults(long timeout, String timeoutMessage) throws E, InterruptedException, TimeoutException { + long now = System.currentTimeMillis(); + final long deadline = now + timeout; + while (results.size() < expectedResultCount && exception == null && now < deadline) { + wait(deadline - now); + now = System.currentTimeMillis(); + } + if (now >= deadline) { + StringBuilder sb = new StringBuilder(); + if (timeoutMessage != null) { + sb.append(timeoutMessage).append(". "); + } + sb.append("MultiResultSyncPoint timeout waiting " + timeout + " ms. Got " + results.size() + " results of " + expectedResultCount + " results"); + + throw new TimeoutException(sb.toString()); + } + if (exception != null) throw exception; + return new ArrayList<>(results); + } + + public synchronized void signal(R result) { + this.results.add(Objects.requireNonNull(result)); + if (expectedResultCount <= results.size()) { + notifyAll(); + } + } + + public synchronized void signal(E exception) { + this.exception = Objects.requireNonNull(exception); + notifyAll(); + } +} diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/util/ResultSyncPoint.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/util/ResultSyncPoint.java index 37b143934..ee691bf90 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/util/ResultSyncPoint.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/util/ResultSyncPoint.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015 Florian Schmaus + * Copyright 2015-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. @@ -26,6 +26,10 @@ public class ResultSyncPoint { private E exception; public R waitForResult(long timeout) throws E, InterruptedException, TimeoutException { + return waitForResult(timeout, null); + } + + public R waitForResult(long timeout, String timeoutMessage) throws E, InterruptedException, TimeoutException { synchronized (this) { if (result != null) { return result; @@ -46,7 +50,12 @@ public class ResultSyncPoint { if (exception != null) { throw exception; } - throw new TimeoutException("Timeout expired"); + + String message = "Timeout after " + timeout + "ms"; + if (timeoutMessage != null) { + message += ": " + timeoutMessage; + } + throw new TimeoutException(message); } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smack/ChatTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smack/ChatTest.java index d956c92ee..56a5ec10e 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smack/ChatTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smack/ChatTest.java @@ -66,8 +66,8 @@ public class ChatTest extends AbstractSmackIntegrationTest { JivePropertiesManager.setJavaObjectEnabled(false); } - @SuppressWarnings("deprecation") @SmackIntegrationTest + @SuppressWarnings({"deprecation", "JavaUtilDate"}) public void testProperties() throws Exception { org.jivesoftware.smack.chat.Chat newChat = chatManagerOne.createChat(conTwo.getUser()); StanzaCollector collector = conTwo.createStanzaCollector(new ThreadFilter(newChat.getThreadID())); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smack/LoginIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smack/LoginIntegrationTest.java index 2c9dbb9ec..1e75154a9 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smack/LoginIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smack/LoginIntegrationTest.java @@ -48,7 +48,7 @@ public class LoginIntegrationTest extends AbstractSmackLowLevelIntegrationTest { * @throws IOException if an I/O error occurred. * @throws SmackException if Smack detected an exceptional situation. * @throws NoSuchAlgorithmException if no such algorithm is available. - * @throws KeyManagementException if there was a key mangement error. + * @throws KeyManagementException if there was a key management error. */ @SmackIntegrationTest public void testInvalidLogin(UnconnectedConnectionSource unconnectedConnectionSource) throws SmackException, IOException, XMPPException, @@ -64,7 +64,8 @@ public class LoginIntegrationTest extends AbstractSmackLowLevelIntegrationTest { () -> connection.login(nonExistentUserString, invalidPassword)); SaslNonza.SASLFailure saslFailure = saslErrorException.getSASLFailure(); - assertEquals(SASLError.not_authorized, saslFailure.getSASLError()); + assertEquals(SASLError.not_authorized, saslFailure.getSASLError(), + "Expected the server to return the appropriate SASL failure condition (but it did not)"); } finally { connection.disconnect(); } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smack/StreamManagementTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smack/StreamManagementTest.java index c9ca9095b..fdbb755c7 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smack/StreamManagementTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smack/StreamManagementTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2020 Florian Schmaus + * Copyright 2015-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. @@ -32,9 +32,11 @@ import org.igniterealtime.smack.inttest.AbstractSmackSpecificLowLevelIntegration import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; import org.igniterealtime.smack.inttest.TestNotPossibleException; import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; +import org.jxmpp.jid.EntityFullJid; public class StreamManagementTest extends AbstractSmackSpecificLowLevelIntegrationTest { + @SuppressWarnings("this-escape") public StreamManagementTest(SmackIntegrationTestEnvironment environment) throws Exception { super(environment, XMPPTCPConnection.class); XMPPTCPConnection connection = getSpecificUnconnectedConnection(); @@ -57,7 +59,7 @@ public class StreamManagementTest extends AbstractSmackSpecificLowLevelIntegrati try { send(body1, conOne, conTwo); - assertMessageWithBodyReceived(body1, collector); + assertMessageWithBodyReceived(body1, collector, conTwo.getUser()); conOne.instantShutdown(); @@ -65,10 +67,10 @@ public class StreamManagementTest extends AbstractSmackSpecificLowLevelIntegrati // Reconnect with xep198 conOne.connect().login(); - assertMessageWithBodyReceived(body2, collector); + assertMessageWithBodyReceived(body2, collector, conTwo.getUser()); send(body3, conOne, conTwo); - assertMessageWithBodyReceived(body3, collector); + assertMessageWithBodyReceived(body3, collector, conTwo.getUser()); } finally { collector.cancel(); @@ -84,9 +86,9 @@ public class StreamManagementTest extends AbstractSmackSpecificLowLevelIntegrati from.sendStanza(message); } - private static void assertMessageWithBodyReceived(String body, StanzaCollector collector) throws InterruptedException { + private static void assertMessageWithBodyReceived(String body, StanzaCollector collector, EntityFullJid recipient) throws InterruptedException { Message message = collector.nextResult(); - assertNotNull(message); - assertEquals(body, message.getBody()); + assertNotNull(message, "Expected '" + recipient + "' to receive a message stanza with body '" + body + "', but it didn't receive the message stanza at all."); + assertEquals(body, message.getBody(), "Expected '" + recipient + "'to receive a message stanza with a specific body, but it received a message stanza with a different body."); } } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smack/WaitForClosingStreamElementTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smack/WaitForClosingStreamElementTest.java index 2326866c7..dc9153aab 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smack/WaitForClosingStreamElementTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smack/WaitForClosingStreamElementTest.java @@ -39,6 +39,6 @@ public class WaitForClosingStreamElementTest extends AbstractSmackLowLevelIntegr Field closingStreamReceivedField = AbstractXMPPConnection.class.getDeclaredField("closingStreamReceived"); closingStreamReceivedField.setAccessible(true); boolean closingStreamReceived = (boolean) closingStreamReceivedField.get(connection); - assertTrue(closingStreamReceived); + assertTrue(closingStreamReceived, "Expected to, but did not, receive a closing stream element on connection " + connection); } } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smack/roster/LowLevelRosterIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smack/roster/LowLevelRosterIntegrationTest.java index 305e9ae47..9f150bd7a 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smack/roster/LowLevelRosterIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smack/roster/LowLevelRosterIntegrationTest.java @@ -49,7 +49,7 @@ public class LowLevelRosterIntegrationTest extends AbstractSmackLowLevelIntegrat final SimpleResultSyncPoint offlineTriggered = new SimpleResultSyncPoint(); - rosterOne.addPresenceEventListener(new AbstractPresenceEventListener() { + final AbstractPresenceEventListener presenceEventListener = new AbstractPresenceEventListener() { @Override public void presenceUnavailable(FullJid jid, Presence presence) { if (!jid.equals(conTwo.getUser())) { @@ -57,15 +57,24 @@ public class LowLevelRosterIntegrationTest extends AbstractSmackLowLevelIntegrat } offlineTriggered.signal(); } - }); + }; + rosterOne.addPresenceEventListener(presenceEventListener); - // Disconnect conTwo, this should cause an 'unavailable' presence to be send from conTwo to - // conOne. - conTwo.disconnect(); + try { + // Disconnect conTwo, this should cause an 'unavailable' presence to be sent from conTwo to + // conOne. + conTwo.disconnect(); - Boolean result = offlineTriggered.waitForResult(timeout); - if (!result) { - throw new Exception("presenceUnavailable() was not called"); + Boolean result = offlineTriggered.waitForResult(timeout); + if (!result) { + throw new Exception("presenceUnavailable() was not called"); + } + } finally { + // Clean up test fixture. + rosterOne.removePresenceEventListener(presenceEventListener); + conTwo.connect(); + conTwo.login(); + IntegrationTestRosterUtil.ensureBothAccountsAreNotInEachOthersRoster(conOne, conTwo); } } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smack/roster/RosterIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smack/roster/RosterIntegrationTest.java index 55aad5c12..98db355c6 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smack/roster/RosterIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smack/roster/RosterIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2020 Florian Schmaus + * Copyright 2015-2024 Florian Schmaus, 2022-2024 Guus der Kinderen * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,23 +16,34 @@ */ package org.jivesoftware.smack.roster; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Collection; import java.util.concurrent.TimeoutException; +import org.jivesoftware.smack.StanzaListener; +import org.jivesoftware.smack.filter.AndFilter; +import org.jivesoftware.smack.filter.FromMatchesFilter; +import org.jivesoftware.smack.filter.PresenceTypeFilter; +import org.jivesoftware.smack.filter.StanzaTypeFilter; import org.jivesoftware.smack.packet.Presence; +import org.jivesoftware.smack.packet.PresenceBuilder; import org.jivesoftware.smack.roster.packet.RosterPacket.ItemType; +import org.jivesoftware.smack.util.Consumer; import org.jivesoftware.smack.util.StringUtils; import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; +import org.igniterealtime.smack.inttest.annotations.SpecificationReference; import org.igniterealtime.smack.inttest.util.IntegrationTestRosterUtil; +import org.igniterealtime.smack.inttest.util.ResultSyncPoint; import org.igniterealtime.smack.inttest.util.SimpleResultSyncPoint; import org.jxmpp.jid.BareJid; import org.jxmpp.jid.Jid; +@SpecificationReference(document = "RFC6121") public class RosterIntegrationTest extends AbstractSmackIntegrationTest { private final Roster rosterOne; @@ -61,7 +72,7 @@ public class RosterIntegrationTest extends AbstractSmackIntegrationTest { final String conTwosRosterName = "ConTwo " + testRunId; final SimpleResultSyncPoint addedAndSubscribed = new SimpleResultSyncPoint(); - rosterOne.addRosterListener(new AbstractRosterListener() { + final RosterListener rosterListener = new AbstractRosterListener() { @Override public void entriesAdded(Collection addresses) { checkIfAddedAndSubscribed(addresses); @@ -78,16 +89,16 @@ public class RosterIntegrationTest extends AbstractSmackIntegrationTest { BareJid bareJid = conTwo.getUser().asBareJid(); RosterEntry rosterEntry = rosterOne.getEntry(bareJid); if (rosterEntry == null) { - addedAndSubscribed.signalFailure("No roster entry for " + bareJid); + addedAndSubscribed.signalFailure("Added/Updated entry was not for " + bareJid); return; } String name = rosterEntry.getName(); if (StringUtils.isNullOrEmpty(name)) { - addedAndSubscribed.signalFailure("Roster entry without name"); + addedAndSubscribed.signalFailure("Added/Updated entry without name"); return; } if (!rosterEntry.getName().equals(conTwosRosterName)) { - addedAndSubscribed.signalFailure("Roster name does not match"); + addedAndSubscribed.signalFailure("Added/Updated entry name does not match. Expected: " + conTwosRosterName + " but was: " + rosterEntry.getName()); return; } if (!rosterEntry.getType().equals(ItemType.to)) { @@ -96,15 +107,327 @@ public class RosterIntegrationTest extends AbstractSmackIntegrationTest { addedAndSubscribed.signal(); } } - }); + }; + + rosterOne.addRosterListener(rosterListener); try { rosterOne.createItemAndRequestSubscription(conTwo.getUser().asBareJid(), conTwosRosterName, null); - - assertTrue(addedAndSubscribed.waitForResult(2 * connection.getReplyTimeout())); + assertResult(addedAndSubscribed, + "A roster entry for " + conTwo.getUser().asBareJid() + " using the name '" + conTwosRosterName + + "' of type 'to' was expected to be added to the roster of " + conOne.getUser() + " (but it was not)."); } finally { rosterTwo.removeSubscribeListener(subscribeListener); + rosterOne.removeRosterListener(rosterListener); + } + } + + /** + * Asserts that when a user sends out a presence subscription request, the server sends a roster push back to the + * user. + * + * @throws Exception when errors occur + */ + @SmackIntegrationTest(section = "3.1.2", quote = + "After locally delivering or remotely routing the presence subscription request, the user's server MUST then " + + "send a roster push to all of the user's interested resources, containing the potential contact with a " + + "subscription state of \"none\" and with notation that the subscription is pending (via an 'ask' attribute " + + "whose value is \"subscribe\").") + public void testRosterPushAfterSubscriptionRequest() throws Exception { + IntegrationTestRosterUtil.ensureBothAccountsAreNotInEachOthersRoster(conOne, conTwo); + rosterTwo.setSubscriptionMode(Roster.SubscriptionMode.manual); // prevents a race condition when asserting the captured roster entry. + final ResultSyncPoint added = new ResultSyncPoint<>(); + + final RosterListener rosterListener = new AbstractRosterListener() { + @Override + public void entriesAdded(Collection addresses) { + for (Jid jid : addresses) { + if (!jid.equals(conTwo.getUser().asBareJid())) { + continue; + } + final BareJid bareJid = conTwo.getUser().asBareJid(); + RosterEntry rosterEntry = rosterOne.getEntry(bareJid); + added.signal(rosterEntry); + return; + } + } + }; + rosterOne.addRosterListener(rosterListener); + + final Presence subscribe = conOne.getStanzaFactory().buildPresenceStanza() + .ofType(Presence.Type.subscribe) + .to(conTwo.getUser().asBareJid()) + .build(); + + try { + conOne.sendStanza(subscribe); + + final RosterEntry rosterEntry = assertResult(added, "Expected the server to send a roster push back to '" + conOne.getUser() + "' after they sent a presence subscription request to '" + conTwo.getUser().asBareJid() + "' (but the server did not)."); + assertEquals(ItemType.none, rosterEntry.getType(), "Unexpected subscription type on roster push after '" + conOne.getUser() + "' sent a presence subscription request to '" + conTwo.getUser().asBareJid() + "'."); + assertTrue(rosterEntry.isSubscriptionPending(), "Missing 'ask=subscribe' attribute on roster push after '" + conOne.getUser() + "' sent a presence subscription request to '" + conTwo.getUser().asBareJid() + "'."); + } finally { + rosterTwo.setSubscriptionMode(Roster.getDefaultSubscriptionMode()); + rosterOne.removeRosterListener(rosterListener); + } + } + + /** + * Asserts that when a user sends out a presence subscription request to an entity for which the user does not have + * a pre-existing subscription, the server will deliver the subscription request to that entity. + * + * @throws Exception when errors occur + */ + @SmackIntegrationTest(section = "3.1.3", quote = + "if there is at least one available resource associated with the contact when the subscription request is " + + "received by the contact's server, then the contact's server MUST send that subscription request to all " + + "available resources in accordance with Section 8.") + public void testPresenceDeliveredToRecipient() throws Exception { + IntegrationTestRosterUtil.ensureBothAccountsAreNotInEachOthersRoster(conOne, conTwo); + + final ResultSyncPoint added = new ResultSyncPoint<>(); + final StanzaListener stanzaListener = stanza -> added.signal((Presence) stanza); + conTwo.addAsyncStanzaListener(stanzaListener, new AndFilter(StanzaTypeFilter.PRESENCE, FromMatchesFilter.createBare(conOne.getUser()))); + + final Presence subscribe = conOne.getStanzaFactory().buildPresenceStanza() + .ofType(Presence.Type.subscribe) + .to(conTwo.getUser().asBareJid()) + .build(); + + try { + conOne.sendStanza(subscribe); + final Presence received = assertResult(added, "Expected subscription request from '" + conOne.getUser() + "' to '" + conTwo.getUser().asBareJid() + "' to be delivered to " + conTwo.getUser() + " (but it did not)."); + assertEquals(Presence.Type.subscribe, received.getType(), "Unexpected presence type in presence stanza received by '" + conTwo.getUser() + "' after '" + conOne.getUser() + "' sent a presence subscription request."); + } finally { + conTwo.removeAsyncStanzaListener(stanzaListener); + } + } + + /** + * Asserts that when a user sends a presence subscription approval, the server stamps the bare JID of the sender, + * and delivers it to the requester. + * + * @throws Exception when errors occur + */ + @SmackIntegrationTest(section = "3.1.5", quote = + "When the contact's client sends the subscription approval, the contact's server MUST stamp the outbound " + + "stanza with the bare JID of the contact and locally deliver or remotely route the " + + "stanza to the user.") + public void testPresenceApprovalStampedAndDelivered() throws Exception { + IntegrationTestRosterUtil.ensureBothAccountsAreNotInEachOthersRoster(conOne, conTwo); + + rosterTwo.setSubscriptionMode(Roster.SubscriptionMode.accept_all); + + // Modify the outbound 'subscribed' stanza, to be 'wrong' (addressed to a full rather than a bare JID), to test if the server overrides this. + final Consumer interceptor = (PresenceBuilder presenceBuilder) -> presenceBuilder.to(conOne.getUser()).build(); + conTwo.addPresenceInterceptor(interceptor, p -> p.getType() == Presence.Type.subscribed); + + final ResultSyncPoint added = new ResultSyncPoint<>(); + final StanzaListener stanzaListener = stanza -> added.signal((Presence) stanza); + + conOne.addAsyncStanzaListener(stanzaListener, PresenceTypeFilter.SUBSCRIBED); + + final Presence subscribe = conOne.getStanzaFactory().buildPresenceStanza() + .ofType(Presence.Type.subscribe) + .to(conTwo.getUser().asBareJid()) + .build(); + + try { + conOne.sendStanza(subscribe); + + final Presence received = assertResult(added, "Expected presence 'subscribed' stanza to be delivered to '" + conOne.getUser() + "' after '" + conTwo.getUser() + "' approved their subscription request (but it was not)."); + assertEquals(conTwo.getUser().asBareJid(), received.getFrom().asEntityBareJidOrThrow(), "Expected presence 'subscribed' stanza that was delivered to '" + conOne.getUser() + "' after '" + conTwo.getUser() + "' approved their subscription request to have a 'from' attribute value that is associated to '" + conTwo.getUser().getLocalpart() + "' (but it did not)."); + assertTrue(received.getFrom().isEntityBareJid(), "Expected presence 'subscribed' stanza that was delivered to '" + conOne.getUser() + "' after '" + conTwo.getUser() + "' approved their subscription request to have a 'from' attribute value that is a bare JID (but it was not)."); + } finally { + rosterTwo.setSubscriptionMode(Roster.getDefaultSubscriptionMode()); + conTwo.removePresenceInterceptor(interceptor); + conOne.removeAsyncStanzaListener(stanzaListener); + } + } + + /** + * Asserts that when a user sends a presence subscription approval, the server sends a roster push to the user with + * a subscription 'from'. + * + * @throws Exception when errors occur + */ + @SmackIntegrationTest(section = "3.1.5", quote = + "The contact's server then MUST send an updated roster push to all of the contact's interested resources, " + + "with the 'subscription' attribute set to a value of \"from\".") + public void testPresenceApprovalYieldsRosterPush() throws Exception { + IntegrationTestRosterUtil.ensureBothAccountsAreNotInEachOthersRoster(conOne, conTwo); + + rosterTwo.setSubscriptionMode(Roster.SubscriptionMode.accept_all); + + final ResultSyncPoint updated = new ResultSyncPoint<>(); + + final RosterListener rosterListener = new AbstractRosterListener() { + @Override + public void entriesAdded(Collection addresses) { + for (Jid jid : addresses) { + if (!jid.equals(conOne.getUser().asBareJid())) { + continue; + } + BareJid bareJid = conOne.getUser().asBareJid(); + RosterEntry rosterEntry = rosterTwo.getEntry(bareJid); + updated.signal(rosterEntry); + } + } + }; + rosterTwo.addRosterListener(rosterListener); + + final Presence subscribe = conOne.getStanzaFactory().buildPresenceStanza() + .ofType(Presence.Type.subscribe) + .to(conTwo.getUser().asBareJid()) + .build(); + + try { + conOne.sendStanza(subscribe); + // The 'subscribe' gets automatically approved by conTwo. + + final RosterEntry entry = assertResult(updated, "Expected '" + conTwo.getUser() + "' to receive a roster push with an update for the entry of '" + conOne.getUser().asBareJid() + "' after '" + conTwo.getUser() + "' approved their subscription request."); + assertEquals(ItemType.from, entry.getType(), "Unexpected type for '" + conOne.getUser().asBareJid() + "''s entry in '" + conTwo.getUser().asBareJid() + "''s roster."); + } finally { + rosterTwo.setSubscriptionMode(Roster.getDefaultSubscriptionMode()); + rosterTwo.removeRosterListener(rosterListener); + } + } + + /** + * Asserts that when a user sends a presence subscription approval, the server sends a roster push to the user with + * a subscription 'both' when the contact already has a subscription to the other entity. + * + * @throws Exception when errors occur + */ + @SmackIntegrationTest(section = "3.1.5", quote = + "The contact's server then MUST send an updated roster push to all of the contact's interested resources, " + + "with the 'subscription' attribute set to a value of \"from\". (Here we assume that the contact does not " + + "already have a subscription to the user; if that were the case, the 'subscription' attribute would be set " + + "to a value of \"both\", as explained under Appendix A.)") + public void testPresenceApprovalYieldsRosterPush2() throws Exception { + IntegrationTestRosterUtil.ensureBothAccountsAreNotInEachOthersRoster(conOne, conTwo); + + // Setup fixture: establish one-way subscription. + rosterOne.setSubscriptionMode(Roster.SubscriptionMode.accept_all); + + final SimpleResultSyncPoint fixtureComplete = new SimpleResultSyncPoint(); + RosterListener rosterListenerTwo = new AbstractRosterListener() { + @Override + public void entriesAdded(Collection addresses) { + checkIfAdded(addresses); + } + @Override + public void entriesUpdated(Collection addresses) { + checkIfAdded(addresses); + } + private void checkIfAdded(Collection addresses) { + for (Jid jid : addresses) { + final BareJid bareJid = conOne.getUser().asBareJid(); + if (!jid.equals(bareJid)) { + continue; + } + if (rosterTwo.getEntry(bareJid) == null) { + continue; + } + if (rosterTwo.getEntry(bareJid).getType() == ItemType.none) { + continue; + } + fixtureComplete.signal(); + rosterTwo.removeRosterListener(this); + } + } + }; + rosterTwo.addRosterListener(rosterListenerTwo); + + final Presence subscribeOne = conTwo.getStanzaFactory().buildPresenceStanza() + .ofType(Presence.Type.subscribe) + .to(conOne.getUser().asBareJid()) + .build(); + try { + conTwo.sendStanza(subscribeOne); + + fixtureComplete.waitForResult(connection.getReplyTimeout()); + } finally { + rosterOne.setSubscriptionMode(Roster.getDefaultSubscriptionMode()); + rosterTwo.removeRosterListener(rosterListenerTwo); + } + + // Setup fixture is now complete. Execute the test. + rosterTwo.setSubscriptionMode(Roster.SubscriptionMode.accept_all); + + final ResultSyncPoint updated = new ResultSyncPoint<>(); + + rosterListenerTwo = new AbstractRosterListener() { + @Override + public void entriesUpdated(Collection addresses) { + for (Jid jid : addresses) { + if (!jid.equals(conOne.getUser().asBareJid())) { + continue; + } + BareJid bareJid = conOne.getUser().asBareJid(); + updated.signal(rosterTwo.getEntry(bareJid)); + } + } + }; + rosterTwo.addRosterListener(rosterListenerTwo); + + final Presence subscribeTwo = conOne.getStanzaFactory().buildPresenceStanza() + .ofType(Presence.Type.subscribe) + .to(conTwo.getUser().asBareJid()) + .build(); + + try { + conOne.sendStanza(subscribeTwo); + + final RosterEntry entry = assertResult(updated, "Expected '" + conTwo.getUser() + "' to receive a roster push with an update for the entry of '" + conOne.getUser().asBareJid() + "' after '" + conOne.getUser() + "' approved their subscription request."); + assertEquals(ItemType.both, entry.getType(), "Unexpected type for '" + conOne.getUser().asBareJid() + "''s entry in '" + conTwo.getUser().asBareJid() + "''s roster."); + } finally { + rosterTwo.setSubscriptionMode(Roster.getDefaultSubscriptionMode()); + rosterTwo.removeRosterListener(rosterListenerTwo); + } + } + + /** + * Asserts that when a presence subscription request is approved, the server sends the latest presence of the now + * subscribed entity to the subscriber. + * + * @throws Exception when errors occur + */ + @SmackIntegrationTest(section = "3.1.5", quote = + "The contact's server MUST then also send current presence to the user from each of the contact's available resources.") + public void testCurrentPresenceSentAfterSubscriptionApproval() throws Exception { + IntegrationTestRosterUtil.ensureBothAccountsAreNotInEachOthersRoster(conOne, conTwo); + + final String needle = "Look for me!"; + conTwo.sendStanza(conTwo.getStanzaFactory().buildPresenceStanza().setStatus(needle).build()); + + rosterTwo.setSubscriptionMode(Roster.SubscriptionMode.accept_all); + + final SimpleResultSyncPoint received = new SimpleResultSyncPoint(); + final StanzaListener stanzaListener = stanza -> { + final Presence presence = (Presence) stanza; + + String status = presence.getStatus(); + if (status == null) return; + + if (status.equals(needle)) { + received.signal(); + } + }; + conOne.addAsyncStanzaListener(stanzaListener, new AndFilter(StanzaTypeFilter.PRESENCE, FromMatchesFilter.createBare(conTwo.getUser()))); + + final Presence subscribe = conOne.getStanzaFactory().buildPresenceStanza() + .ofType(Presence.Type.subscribe) + .to(conTwo.getUser().asBareJid()) + .build(); + + try { + conOne.sendStanza(subscribe); + + assertResult(received, "Expected '" + conTwo.getUser() + "' to receive '" + conOne.getUser() + "''s current presence update (including the status '" + needle + "'), but it did not."); + } finally { + rosterTwo.setSubscriptionMode(Roster.getDefaultSubscriptionMode()); + conOne.removeAsyncStanzaListener(stanzaListener); } } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/caps/EntityCapsTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/caps/EntityCapsTest.java index 4c373a9ce..74d9aa6a8 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/caps/EntityCapsTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/caps/EntityCapsTest.java @@ -18,6 +18,7 @@ package org.jivesoftware.smackx.caps; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -51,7 +52,9 @@ import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; import org.igniterealtime.smack.inttest.annotations.AfterClass; import org.igniterealtime.smack.inttest.annotations.BeforeClass; import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; +import org.igniterealtime.smack.inttest.annotations.SpecificationReference; +@SpecificationReference(document = "XEP-0115", version = "1.6.0") public class EntityCapsTest extends AbstractSmackIntegrationTest { private final EntityCapsManager ecmTwo; @@ -94,7 +97,9 @@ public class EntityCapsTest extends AbstractSmackIntegrationTest { public void testLocalEntityCaps() throws InterruptedException, NoResponseException, XMPPErrorException, NotConnectedException { final String dummyFeature = getNewDummyFeature(); DiscoverInfo info = EntityCapsManager.getDiscoveryInfoByNodeVer(ecmTwo.getLocalNodeVer()); - assertFalse(info.containsFeature(dummyFeature)); + assertFalse(info.containsFeature(dummyFeature), + "Expected the service discovery info for node '" + ecmTwo.getLocalNodeVer() + + "' to contain the feature '" + dummyFeature + "' (but it did not)."); // TODO Shouldn't this assertion be in a unit test instead of an integration test? dropWholeEntityCapsCache(); @@ -118,8 +123,12 @@ public class EntityCapsTest extends AbstractSmackIntegrationTest { // The other connection has to receive this stanza and record the // information in order for this test to succeed. info = EntityCapsManager.getDiscoveryInfoByNodeVer(ecmTwo.getLocalNodeVer()); - assertNotNull(info); - assertTrue(info.containsFeature(dummyFeature)); + assertNotNull(info, + "Expected '" + conOne.getUser() + "' to have received an 'available' presence from '" + conTwo.getUser() + + "' with a new CAPS 'ver' attribute (but it did not)."); + assertTrue(info.containsFeature(dummyFeature), + "Expected the service discovery info for node '" + ecmTwo.getLocalNodeVer() + + "' to contain the feature '" + dummyFeature + "' (but it did not)."); // TODO As above: shouldn't this assertion be in a unit test instead of an integration test? } /** @@ -146,7 +155,7 @@ public class EntityCapsTest extends AbstractSmackIntegrationTest { // discover that DiscoverInfo info = sdmOne.discoverInfo(conTwo.getUser()); // that discovery should cause a disco#info - assertTrue(discoInfoSend.get()); + assertTrue(discoInfoSend.get(), "Expected '" + conOne.getUser() + "' to have made a disco/info request to '" + conTwo.getUser() + "', but it did not."); assertTrue(info.containsFeature(dummyFeature), "The info response '" + info + "' does not contain the expected feature '" + dummyFeature + '\''); discoInfoSend.set(false); @@ -154,8 +163,9 @@ public class EntityCapsTest extends AbstractSmackIntegrationTest { // discover that info = sdmOne.discoverInfo(conTwo.getUser()); // that discovery shouldn't cause a disco#info - assertFalse(discoInfoSend.get()); - assertTrue(info.containsFeature(dummyFeature)); + assertFalse(discoInfoSend.get(), "Expected '" + conOne.getUser() + "' to not have made a disco/info request to '" + conTwo.getUser() + "' (as CAPS should have been cached), but it did not."); + assertTrue(info.containsFeature(dummyFeature), + "The info response '" + info + "' does not contain the expected feature '" + dummyFeature + '\''); } @SmackIntegrationTest @@ -165,7 +175,8 @@ public class EntityCapsTest extends AbstractSmackIntegrationTest { addFeatureAndWaitForPresence(conOne, conTwo, dummyFeature); String nodeVerAfter = EntityCapsManager.getNodeVersionByJid(conTwo.getUser()); - assertFalse(nodeVerBefore.equals(nodeVerAfter)); + assertNotEquals(nodeVerBefore, nodeVerAfter, + "Expected the reported node 'ver' value to differ after a feature was added (but it did not)."); } @SmackIntegrationTest @@ -191,12 +202,12 @@ public class EntityCapsTest extends AbstractSmackIntegrationTest { DiscoverInfo info = sdmOne.discoverInfo(conTwo.getUser()); String u1ver = EntityCapsManager.getNodeVersionByJid(conTwo.getUser()); - assertNotNull(u1ver); + assertNotNull(u1ver, "Expected " + conOne.getUser() + " to have received a CAPS 'ver' value for " + conTwo.getUser() + " (but did not)."); DiscoverInfo entityInfo = EntityCapsManager.CAPS_CACHE.lookup(u1ver); - assertNotNull(entityInfo); + assertNotNull(entityInfo, "Expected the local static cache to have a value cached for 'ver' value '" + u1ver + "' (but it did not)."); - assertEquals(info.toXML().toString(), entityInfo.toXML().toString()); + assertEquals(info.toXML().toString(), entityInfo.toXML().toString(), "Expected the cached service/discovery info to be equal to the original (but it was not)."); } private static void dropWholeEntityCapsCache() { diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/chatstate/ChatStateIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/chatstate/ChatStateIntegrationTest.java index 11e1340f4..2c1b56e1a 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/chatstate/ChatStateIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/chatstate/ChatStateIntegrationTest.java @@ -27,8 +27,10 @@ import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; import org.igniterealtime.smack.inttest.annotations.AfterClass; import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; +import org.igniterealtime.smack.inttest.annotations.SpecificationReference; import org.igniterealtime.smack.inttest.util.SimpleResultSyncPoint; +@SpecificationReference(document = "XEP-0085", version = "2.1") public class ChatStateIntegrationTest extends AbstractSmackIntegrationTest { // Listener for composing chat state @@ -73,7 +75,7 @@ public class ChatStateIntegrationTest extends AbstractSmackIntegrationTest { Chat chat = ChatManager.getInstanceFor(conOne) .chatWith(conTwo.getUser().asEntityBareJid()); chat.send("Hi!"); - activeSyncPoint.waitForResult(timeout); + assertResult(activeSyncPoint, "Expected " + conTwo.getUser() + " to receive an 'active' chat state from " + conOne + " (but they did not)."); } @AfterClass 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..1ece51611 --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/commands/AdHocCommandIntegrationTest.java @@ -0,0 +1,364 @@ +/** + * + * Copyright 2023-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.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; +import org.igniterealtime.smack.inttest.annotations.SpecificationReference; + +@SpecificationReference(document = "XEP-0050", version = "1.3.0") +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, "Expected a field named 'my-field' to exist in the form that " + + conTwo.getUser() + " obtained from " + conOne.getUser() + "'s command node '" + commandNode + + "' (but it did not)."); + } 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("Arithmetic 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(); + } + + 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, + "Unexpected value in the field 'result' from the command result that " + conTwo.getUser() + + " received from " + conOne.getUser() + " after completing a multi-staged ad-hoc command on node '" + + commandNode + "'."); + } 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, + "Unexpected value in the field 'result' from the command result that " + conTwo.getUser() + + " received from " + conOne.getUser() + " after completing a multi-staged ad-hoc command on node '" + + commandNode + "'."); + } 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(StanzaError.Condition.bad_request, exception.getStanzaError().getCondition(), + "Unexpected error condition received after " + conTwo.getUser() + " supplied an invalid argument " + + "to the command node '" + commandNode + "' of " + conOne.getUser()); + } 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/filetransfer/FileTransferIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/filetransfer/FileTransferIntegrationTest.java index ac79e46ec..25ebb9b2a 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/filetransfer/FileTransferIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/filetransfer/FileTransferIntegrationTest.java @@ -31,8 +31,10 @@ import org.jivesoftware.smackx.filetransfer.FileTransfer.Status; import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; -import org.igniterealtime.smack.inttest.util.ResultSyncPoint; +import org.igniterealtime.smack.inttest.annotations.SpecificationReference; +import org.igniterealtime.smack.inttest.util.SimpleResultSyncPoint; +@SpecificationReference(document = "XEP-0096", version = "1.3.1") public class FileTransferIntegrationTest extends AbstractSmackIntegrationTest { private static final int MAX_FT_DURATION = 360; @@ -65,7 +67,7 @@ public class FileTransferIntegrationTest extends AbstractSmackIntegrationTest { } private void genericfileTransferTest() throws Exception { - final ResultSyncPoint resultSyncPoint = new ResultSyncPoint<>(); + final SimpleResultSyncPoint resultSyncPoint = new SimpleResultSyncPoint(); final FileTransferListener receiveListener = new FileTransferListener() { @Override public void fileTransferRequest(FileTransferRequest request) { @@ -82,7 +84,7 @@ public class FileTransferIntegrationTest extends AbstractSmackIntegrationTest { os.flush(); dataReceived = os.toByteArray(); if (Arrays.equals(dataToSend, dataReceived)) { - resultSyncPoint.signal("Received data matches send data. \\o/"); + resultSyncPoint.signal(); } else { resultSyncPoint.signal(new Exception("Received data does not match")); @@ -115,7 +117,9 @@ public class FileTransferIntegrationTest extends AbstractSmackIntegrationTest { } } - resultSyncPoint.waitForResult(MAX_FT_DURATION * 1000); + assertResult(resultSyncPoint, MAX_FT_DURATION * 1000, + "Expected data to be transferred successfully from " + conOne.getUser() + " to " + conTwo.getUser() + + " (but it did not)."); ftManagerTwo.removeFileTransferListener(receiveListener); } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/geolocation/GeolocationIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/geolocation/GeolocationIntegrationTest.java index 7e149ff3b..1cd1d3b8d 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/geolocation/GeolocationIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/geolocation/GeolocationIntegrationTest.java @@ -17,7 +17,6 @@ package org.jivesoftware.smackx.geolocation; import java.net.URI; -import java.util.concurrent.TimeoutException; import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NotConnectedException; @@ -34,11 +33,12 @@ import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; import org.igniterealtime.smack.inttest.annotations.AfterClass; import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; +import org.igniterealtime.smack.inttest.annotations.SpecificationReference; import org.igniterealtime.smack.inttest.util.IntegrationTestRosterUtil; import org.igniterealtime.smack.inttest.util.SimpleResultSyncPoint; -import org.junit.jupiter.api.Assertions; import org.jxmpp.util.XmppDateTime; +@SpecificationReference(document = "XEP-0080", version = "1.9") public class GeolocationIntegrationTest extends AbstractSmackIntegrationTest { private final GeoLocationManager glm1; @@ -106,16 +106,12 @@ public class GeolocationIntegrationTest extends AbstractSmackIntegrationTest { glm1.publishGeoLocation(data); // for the purpose of this test, this needs not be blocking/use publishAndWait(); // Wait for the data to be received. - try { - Object result = geoLocationReceived.waitForResult(timeout); - - // Explicitly assert the success case. - Assertions.assertNotNull(result, "Expected to receive a PEP notification, but did not."); - } catch (TimeoutException e) { - Assertions.fail("Expected to receive a PEP notification, but did not."); - } + assertResult(geoLocationReceived, + "Expected " + conTwo.getUser() + " to receive a PEP notification from " + conOne.getUser() + + " that contained '" + data.toXML() + "', but did not."); } finally { unregisterListener(glm2, geoLocationListener); + IntegrationTestRosterUtil.ensureBothAccountsAreNotInEachOthersRoster(conOne, conTwo); } } @@ -171,16 +167,12 @@ public class GeolocationIntegrationTest extends AbstractSmackIntegrationTest { registerListenerAndWait(glm2, ServiceDiscoveryManager.getInstanceFor(conTwo), geoLocationListener); // Wait for the data to be received. - try { - Object result = geoLocationReceived.waitForResult(timeout); - - // Explicitly assert the success case. - Assertions.assertNotNull(result, "Expected to receive a PEP notification, but did not."); - } catch (TimeoutException e) { - Assertions.fail("Expected to receive a PEP notification, but did not."); - } + assertResult(geoLocationReceived, + "Expected " + conTwo.getUser() + " to receive a PEP notification from " + conOne.getUser() + + " that contained '" + data.toXML() + "', but did not."); } finally { unregisterListener(glm2, geoLocationListener); + IntegrationTestRosterUtil.ensureBothAccountsAreNotInEachOthersRoster(conOne, conTwo); } } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/httpfileupload/HttpFileUploadIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/httpfileupload/HttpFileUploadIntegrationTest.java index 243bfec36..862a06af2 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/httpfileupload/HttpFileUploadIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/httpfileupload/HttpFileUploadIntegrationTest.java @@ -36,7 +36,9 @@ import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; import org.igniterealtime.smack.inttest.TestNotPossibleException; import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; +import org.igniterealtime.smack.inttest.annotations.SpecificationReference; +@SpecificationReference(document = "XEP-0363", version = "0.4.0") public class HttpFileUploadIntegrationTest extends AbstractSmackIntegrationTest { private static final int FILE_SIZE = 1024 * 128; @@ -80,7 +82,7 @@ public class HttpFileUploadIntegrationTest extends AbstractSmackIntegrationTest URL getUrl = hfumOne.uploadFile(file, new UploadProgressListener() { @Override public void onUploadProgress(long uploadedBytes, long totalBytes) { - double progress = uploadedBytes / totalBytes; + double progress = uploadedBytes / ((double) totalBytes); LOGGER.fine("HTTP File Upload progress " + progress + "% (" + uploadedBytes + '/' + totalBytes + ')'); } }); @@ -102,6 +104,6 @@ public class HttpFileUploadIntegrationTest extends AbstractSmackIntegrationTest byte[] downBytes = baos.toByteArray(); - assertArrayEquals(upBytes, downBytes); + assertArrayEquals(upBytes, downBytes, "Expected the downloaded bytes to be equal to the uploaded bytes (but they were not)."); } } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/IoTControlIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/IoTControlIntegrationTest.java index 498493b5b..4d4844adf 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/IoTControlIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/IoTControlIntegrationTest.java @@ -33,10 +33,12 @@ import org.jivesoftware.smackx.iot.control.element.SetData; import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; +import org.igniterealtime.smack.inttest.annotations.SpecificationReference; import org.igniterealtime.smack.inttest.util.IntegrationTestRosterUtil; import org.igniterealtime.smack.inttest.util.SimpleResultSyncPoint; import org.jxmpp.jid.Jid; +@SpecificationReference(document = "XEP-0347", version = "0.5.1") public class IoTControlIntegrationTest extends AbstractSmackIntegrationTest { private final IoTControlManager IoTControlManagerOne; @@ -87,7 +89,7 @@ public class IoTControlIntegrationTest extends AbstractSmackIntegrationTest { SetData data = new SetBoolData(testRunId, true); IoTSetResponse response = IoTControlManagerTwo.setUsingIq(conOne.getUser(), data); - assertNotNull(response); + assertNotNull(response, "Expected " + conOne.getUser() + " to receive an IQ response with an 'setResponse' child element, but no such response was received."); } finally { IoTControlManagerOne.uninstallThing(controlThing); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/IoTDataIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/IoTDataIntegrationTest.java index 858eae9a7..731260475 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/IoTDataIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/IoTDataIntegrationTest.java @@ -37,8 +37,10 @@ import org.jivesoftware.smackx.iot.data.element.TimestampElement; import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; +import org.igniterealtime.smack.inttest.annotations.SpecificationReference; import org.igniterealtime.smack.inttest.util.IntegrationTestRosterUtil; +@SpecificationReference(document = "XEP-0347", version = "0.5.1") public class IoTDataIntegrationTest extends AbstractSmackIntegrationTest { private final IoTDataManager iotDataManagerOne; @@ -84,23 +86,23 @@ public class IoTDataIntegrationTest extends AbstractSmackIntegrationTest { IntegrationTestRosterUtil.ensureBothAccountsAreNotInEachOthersRoster(conOne, conTwo); } - assertEquals(1, values.size()); + assertEquals(1, values.size(), "An unexpected amount of momentary values was received by " + conOne.getUser()); IoTFieldsExtension iotFieldsExtension = values.get(0); List nodes = iotFieldsExtension.getNodes(); - assertEquals(1, nodes.size()); + assertEquals(1, nodes.size(), "The momentary value received by " + conOne.getUser() + " contains an unexpected amount of nodes."); NodeElement node = nodes.get(0); List timestamps = node.getTimestampElements(); - assertEquals(1, timestamps.size()); + assertEquals(1, timestamps.size(), "The node received by " + conOne.getUser() + " contains an unexpected amount of timestamps."); TimestampElement timestamp = timestamps.get(0); List fields = timestamp.getDataFields(); - assertEquals(1, fields.size()); + assertEquals(1, fields.size(), "The timestamp received by " + conOne.getUser() + " contains an unexpected amount of data fields."); IoTDataField dataField = fields.get(0); - assertTrue(dataField instanceof IoTDataField.IntField); + assertTrue(dataField instanceof IoTDataField.IntField, "The data field received by " + conOne.getUser() + " was expected to be an instance of " + IoTDataField.IntField.class.getSimpleName() + ", but instead, it was " + dataField.getClass().getSimpleName()); IoTDataField.IntField intDataField = (IoTDataField.IntField) dataField; - assertEquals(testRunId, intDataField.getName()); - assertEquals(value, intDataField.getValue()); + assertEquals(testRunId, intDataField.getName(), "Unexpected name in the data field received by " + conOne.getUser()); + assertEquals(value, intDataField.getValue(), "Unexpected value in the data field received by " + conOne.getUser()); } } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/IoTDiscoveryIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/IoTDiscoveryIntegrationTest.java index df6bf345e..7c3f3dcac 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/IoTDiscoveryIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/IoTDiscoveryIntegrationTest.java @@ -34,8 +34,10 @@ import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; import org.igniterealtime.smack.inttest.TestNotPossibleException; import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; +import org.igniterealtime.smack.inttest.annotations.SpecificationReference; import org.jxmpp.jid.Jid; +@SpecificationReference(document = "XEP-0347", version = "0.5.1") public class IoTDiscoveryIntegrationTest extends AbstractSmackIntegrationTest { private final IoTDiscoveryManager discoveryManagerOne; @@ -60,7 +62,7 @@ public class IoTDiscoveryIntegrationTest extends AbstractSmackIntegrationTest { registerThing(discoveryManagerOne, thing); IoTClaimed iotClaimed = discoveryManagerTwo.claimThing(thing.getMetaTags()); - assertEquals(conOne.getUser().asBareJid(), iotClaimed.getJid()); + assertEquals(conOne.getUser().asBareJid(), iotClaimed.getJid(), "Thing claimed by an unexpected JID"); discoveryManagerTwo.disownThing(iotClaimed.getJid()); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/iqversion/VersionIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/iqversion/VersionIntegrationTest.java index 9e903bbb8..05c8fa18d 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/iqversion/VersionIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/iqversion/VersionIntegrationTest.java @@ -28,7 +28,9 @@ import org.jivesoftware.smackx.iqversion.packet.Version; import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; +import org.igniterealtime.smack.inttest.annotations.SpecificationReference; +@SpecificationReference(document = "XEP-0092", version = "1.1") public class VersionIntegrationTest extends AbstractSmackIntegrationTest { public VersionIntegrationTest(SmackIntegrationTestEnvironment environment) { @@ -45,8 +47,8 @@ public class VersionIntegrationTest extends AbstractSmackIntegrationTest { final String versionName = "Smack Integration Test " + testRunId; versionManagerTwo.setVersion(versionName, "1.0"); - assertTrue (versionManagerOne.isSupported(conTwo.getUser())); + assertTrue(versionManagerOne.isSupported(conTwo.getUser()), "Expected " + conTwo.getUser() + " to support " + Version.NAMESPACE + " (but it does not)."); Version version = versionManagerOne.getVersion(conTwo.getUser()); - assertEquals(versionName, version.getName()); + assertEquals(versionName, version.getName(), "Unexpected version name reported by " + conTwo.getUser()); } } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/mam/MamIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/mam/MamIntegrationTest.java index 046a33ac6..9b9cba7a3 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/mam/MamIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/mam/MamIntegrationTest.java @@ -42,9 +42,11 @@ import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; import org.igniterealtime.smack.inttest.TestNotPossibleException; import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; +import org.igniterealtime.smack.inttest.annotations.SpecificationReference; import org.igniterealtime.smack.inttest.util.SimpleResultSyncPoint; import org.jxmpp.jid.EntityBareJid; +@SpecificationReference(document = "XEP-0313", version = "0.6.3") public class MamIntegrationTest extends AbstractSmackIntegrationTest { private final MamManager mamManagerConTwo; @@ -113,14 +115,14 @@ public class MamIntegrationTest extends AbstractSmackIntegrationTest { .build(); MamQuery mamQuery = mamManagerConTwo.queryArchive(mamQueryArgs); - assertEquals(1, mamQuery.getMessages().size()); + assertEquals(1, mamQuery.getMessages().size(), conTwo.getUser() + " received an unexpected amount of messages in response to a MAM query."); Message mamMessage = mamQuery.getMessages().get(0); - assertEquals(messageId, mamMessage.getStanzaId()); - assertEquals(messageBody, mamMessage.getBody()); - assertEquals(conOne.getUser(), mamMessage.getFrom()); - assertEquals(userTwo, mamMessage.getTo()); + assertEquals(messageId, mamMessage.getStanzaId(), "The message received by " + conTwo.getUser() + " via a MAM query has an unexpected stanza ID."); + assertEquals(messageBody, mamMessage.getBody(), "The message received by " + conTwo.getUser() + " via a MAM query has an unexpected body."); + assertEquals(conOne.getUser(), mamMessage.getFrom(), "The message received by " + conTwo.getUser() + " via a MAM query has an unexpected from-attribute value."); + assertEquals(userTwo, mamMessage.getTo(), "The message received by " + conTwo.getUser() + " via a MAM query has an unexpected to-attribute value."); } @SmackIntegrationTest @@ -174,8 +176,8 @@ public class MamIntegrationTest extends AbstractSmackIntegrationTest { MamQuery mamQuery = mamManagerConTwo.queryArchive(mamQueryArgs); - assertFalse(mamQuery.isComplete()); - assertEquals(messagesPerPage, mamQuery.getMessageCount()); + assertFalse(mamQuery.isComplete(), "Expected the first MAM response received by " + conTwo.getUser() + " to NOT be complete (but it was)."); + assertEquals(messagesPerPage, mamQuery.getMessageCount(), "Unexpected message count in MAM response received by " + conTwo.getUser()); List> pages = new ArrayList<>(numPages); pages.add(mamQuery.getMessages()); @@ -185,12 +187,12 @@ public class MamIntegrationTest extends AbstractSmackIntegrationTest { boolean isLastQuery = additionalPageRequestNum == numPages - 2; if (isLastQuery) { - assertTrue(mamQuery.isComplete()); + assertTrue(mamQuery.isComplete(), "Expected the last MAM response received by " + conTwo.getUser() + " to be complete (but it was not)."); } else { - assertFalse(mamQuery.isComplete()); + assertFalse(mamQuery.isComplete(), "Expected an intermediate MAM response received by " + conTwo.getUser() + " to NOT be complete (but it was)."); } - assertEquals(messagesPerPage, page.size()); + assertEquals(messagesPerPage, page.size(), "Unexpected amount of messages in the MAM response page received by " + conTwo.getUser()); pages.add(page); } @@ -200,13 +202,13 @@ public class MamIntegrationTest extends AbstractSmackIntegrationTest { queriedMessages.addAll(messages); } - assertEquals(outgoingMessages.size(), queriedMessages.size()); + assertEquals(outgoingMessages.size(), queriedMessages.size(), "An unexpected total number of messages was received through MAM by " + conTwo.getUser()); for (int i = 0; i < outgoingMessages.size(); i++) { Message outgoingMessage = outgoingMessages.get(i); Message queriedMessage = queriedMessages.get(i); - assertEquals(outgoingMessage.getBody(), queriedMessage.getBody()); + assertEquals(outgoingMessage.getBody(), queriedMessage.getBody(), "Unexpected message body for message number " + (i + 1) + " as received by " + conTwo.getUser() + " (are messages received out of order?)"); } } } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/mood/MoodIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/mood/MoodIntegrationTest.java index 1a83085dc..c430938db 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/mood/MoodIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/mood/MoodIntegrationTest.java @@ -16,8 +16,6 @@ */ package org.jivesoftware.smackx.mood; -import java.util.concurrent.TimeoutException; - import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.XMPPException; @@ -30,10 +28,11 @@ import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; import org.igniterealtime.smack.inttest.annotations.AfterClass; import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; +import org.igniterealtime.smack.inttest.annotations.SpecificationReference; import org.igniterealtime.smack.inttest.util.IntegrationTestRosterUtil; import org.igniterealtime.smack.inttest.util.SimpleResultSyncPoint; -import org.junit.jupiter.api.Assertions; +@SpecificationReference(document = "XEP-0107", version = "1.2.1") public class MoodIntegrationTest extends AbstractSmackIntegrationTest { private final MoodManager mm1; @@ -80,13 +79,10 @@ public class MoodIntegrationTest extends AbstractSmackIntegrationTest { mm1.setMood(data); // for the purpose of this test, this needs not be blocking/use publishAndWait(); // Wait for the data to be received. - try { - moodReceived.waitForResult(timeout); - } catch (TimeoutException e) { - Assertions.fail("Expected to receive a PEP notification, but did not."); - } + assertResult(moodReceived, "Expected " + conTwo.getUser() + " to receive a PEP notification, but did not."); } finally { unregisterListener(mm2, moodListener); + IntegrationTestRosterUtil.ensureBothAccountsAreNotInEachOthersRoster(conOne, conTwo); } } @@ -119,16 +115,10 @@ public class MoodIntegrationTest extends AbstractSmackIntegrationTest { registerListenerAndWait(mm2, ServiceDiscoveryManager.getInstanceFor(conTwo), moodListener); // Wait for the data to be received. - try { - Object result = moodReceived.waitForResult(timeout); - - // Explicitly assert the success case. - Assertions.assertNotNull(result, "Expected to receive a PEP notification, but did not."); - } catch (TimeoutException e) { - Assertions.fail("Expected to receive a PEP notification, but did not."); - } + assertResult(moodReceived, "Expected " + conTwo.getUser() + " to receive a PEP notification, but did not."); } finally { unregisterListener(mm2, moodListener); + IntegrationTestRosterUtil.ensureBothAccountsAreNotInEachOthersRoster(conOne, conTwo); } } 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..9465ef14a 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-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. @@ -18,10 +18,18 @@ package org.jivesoftware.smackx.muc; import java.util.List; +import java.util.logging.Level; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.XMPPException.XMPPErrorException; +import org.jivesoftware.smack.packet.StanzaError; import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smackx.muc.MultiUserChatException.MissingMucCreationAcknowledgeException; +import org.jivesoftware.smackx.muc.MultiUserChatException.MucAlreadyJoinedException; +import org.jivesoftware.smackx.muc.MultiUserChatException.NotAMucServiceException; +import org.jivesoftware.smackx.xdata.form.FillableForm; +import org.jivesoftware.smackx.xdata.form.Form; import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; @@ -34,7 +42,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); @@ -45,7 +53,7 @@ public class AbstractMultiUserChatIntegrationTest extends AbstractSmackIntegrati public AbstractMultiUserChatIntegrationTest(SmackIntegrationTestEnvironment environment) throws SmackException.NoResponseException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, - InterruptedException, TestNotPossibleException { + InterruptedException, TestNotPossibleException, MucAlreadyJoinedException, MissingMucCreationAcknowledgeException, NotAMucServiceException, XmppStringprepException { super(environment); mucManagerOne = MultiUserChatManager.getInstanceFor(conOne); mucManagerTwo = MultiUserChatManager.getInstanceFor(conTwo); @@ -53,10 +61,40 @@ public class AbstractMultiUserChatIntegrationTest extends AbstractSmackIntegrati List services = mucManagerOne.getMucServiceDomains(); if (services.isEmpty()) { - throw new TestNotPossibleException("No MUC (XEP-45) service found"); + throw new TestNotPossibleException("No MUC (XEP-0045) service found"); } - mucService = services.get(0); + DomainBareJid needle = null; + for (final DomainBareJid service : services) { + MultiUserChat multiUserChat = null; + try { + String roomNameLocal = String.join("-", "smack-inttest-abstract", testRunId, StringUtils.insecureRandomString(6)); + EntityBareJid mucAddress = JidCreate.entityBareFrom(Localpart.from(roomNameLocal), service.getDomain()); + multiUserChat = mucManagerOne.getMultiUserChat(mucAddress); + + createMuc(multiUserChat, "test"); + + needle = service; + break; + } catch (XMPPException.XMPPErrorException e) { + mucCreationDisallowedOrThrow(e); + LOGGER.log(Level.FINER, "MUC service " + service + " does not allow MUC creation", e); + } finally { + tryDestroy(multiUserChat); + } + } + + if (needle == null) { + throw new TestNotPossibleException("No MUC (XEP-0045) service found that allows test users to createa new room. Considered MUC services: " + services); + } + mucService = needle; + } + + static void mucCreationDisallowedOrThrow(XMPPException.XMPPErrorException e) throws XMPPErrorException { + StanzaError.Condition condition = e.getStanzaError().getCondition(); + if (condition == StanzaError.Condition.not_allowed) + return; + throw e; } /** @@ -140,4 +178,58 @@ public class AbstractMultiUserChatIntegrationTest extends AbstractSmackIntegrati .makeHidden() .submitConfigurationForm(); } + + /** + * Creates a non-anonymous room. + * + *

    From XEP-0045 § 10.1.3:

    + *
    + * Note: The _whois configuration option specifies whether the room is non-anonymous (a value of "anyone"), + * semi-anonymous (a value of "moderators"), or fully anonymous (a value of "none", not shown here). + *
    + */ + static void createNonAnonymousMuc(MultiUserChat muc, Resourcepart resourceName) throws SmackException.NoResponseException, XMPPException.XMPPErrorException, InterruptedException, MultiUserChatException.MucAlreadyJoinedException, SmackException.NotConnectedException, MultiUserChatException.MissingMucCreationAcknowledgeException, MultiUserChatException.NotAMucServiceException { + muc.create(resourceName); + Form configForm = muc.getConfigurationForm(); + FillableForm answerForm = configForm.getFillableForm(); + answerForm.setAnswer("muc#roomconfig_whois", "anyone"); + muc.sendConfigurationForm(answerForm); + } + + /** + * Creates a semi-anonymous room. + * + *

    From XEP-0045 § 10.1.3:

    + *
    + * Note: The _whois configuration option specifies whether the room is non-anonymous (a value of "anyone"), + * semi-anonymous (a value of "moderators"), or fully anonymous (a value of "none", not shown here). + *
    + */ + static void createSemiAnonymousMuc(MultiUserChat muc, Resourcepart resourceName) throws SmackException.NoResponseException, XMPPException.XMPPErrorException, InterruptedException, MultiUserChatException.MucAlreadyJoinedException, SmackException.NotConnectedException, MultiUserChatException.MissingMucCreationAcknowledgeException, MultiUserChatException.NotAMucServiceException { + muc.create(resourceName); + Form configForm = muc.getConfigurationForm(); + FillableForm answerForm = configForm.getFillableForm(); + answerForm.setAnswer("muc#roomconfig_whois", "moderators"); + muc.sendConfigurationForm(answerForm); + } + + /** + * Creates a password-protected room. + */ + static void createPasswordProtectedMuc(MultiUserChat muc, Resourcepart resourceName, String password) throws SmackException.NoResponseException, XMPPException.XMPPErrorException, InterruptedException, MultiUserChatException.MucAlreadyJoinedException, SmackException.NotConnectedException, MultiUserChatException.MissingMucCreationAcknowledgeException, MultiUserChatException.NotAMucServiceException { + muc.create(resourceName); + Form configForm = muc.getConfigurationForm(); + FillableForm answerForm = configForm.getFillableForm(); + answerForm.setAnswer("muc#roomconfig_passwordprotectedroom", true); + answerForm.setAnswer("muc#roomconfig_roomsecret", password); + muc.sendConfigurationForm(answerForm); + } + + static void setMaxUsers(MultiUserChat muc, int maxUsers) throws SmackException.NoResponseException, XMPPException.XMPPErrorException, InterruptedException, SmackException.NotConnectedException { + Form configForm = muc.getConfigurationForm(); + FillableForm answerForm = configForm.getFillableForm(); + answerForm.setAnswer("muc#roomconfig_maxusers", maxUsers); + muc.sendConfigurationForm(answerForm); + } + } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatEntityIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatEntityIntegrationTest.java index 279160c0f..aa30d5b78 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatEntityIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatEntityIntegrationTest.java @@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.List; import java.util.Map; import org.jivesoftware.smack.SmackException; @@ -30,55 +31,71 @@ import org.jivesoftware.smack.packet.StanzaError; import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; import org.jivesoftware.smackx.disco.packet.DiscoverInfo; import org.jivesoftware.smackx.disco.packet.DiscoverItems; +import org.jivesoftware.smackx.muc.MultiUserChatException.MissingMucCreationAcknowledgeException; +import org.jivesoftware.smackx.muc.MultiUserChatException.MucAlreadyJoinedException; +import org.jivesoftware.smackx.muc.MultiUserChatException.NotAMucServiceException; +import org.jivesoftware.smackx.muc.packet.MUCInitialPresence; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; import org.igniterealtime.smack.inttest.TestNotPossibleException; import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; +import org.igniterealtime.smack.inttest.annotations.SpecificationReference; + +import org.jxmpp.jid.DomainBareJid; import org.jxmpp.jid.EntityBareJid; import org.jxmpp.jid.EntityFullJid; import org.jxmpp.jid.parts.Resourcepart; +import org.jxmpp.stringprep.XmppStringprepException; +@SpecificationReference(document = "XEP-0045", version = "1.34.6") public class MultiUserChatEntityIntegrationTest extends AbstractMultiUserChatIntegrationTest { public MultiUserChatEntityIntegrationTest(SmackIntegrationTestEnvironment environment) throws SmackException.NoResponseException, XMPPException.XMPPErrorException, - SmackException.NotConnectedException, InterruptedException, TestNotPossibleException { + SmackException.NotConnectedException, InterruptedException, TestNotPossibleException, MucAlreadyJoinedException, MissingMucCreationAcknowledgeException, NotAMucServiceException, XmppStringprepException { super(environment); } /** - * Asserts that a MUC service can have its features discovered - * - *

    From XEP-0045 § 6.2:

    - *
    - * An entity may wish to discover if a service implements the Multi-User Chat protocol; in order to do so, it - * sends a service discovery information ("disco#info") query to the MUC service's JID. The service MUST return - * its identity and the features it supports. - *
    + * Asserts that a MUC service can be discovered. * * @throws Exception when errors occur */ - @SmackIntegrationTest + @SmackIntegrationTest(section = "6.1", quote = + "An entity often discovers a MUC service by sending a Service Discovery items (\"disco#items\") request to " + + "its own server. The server then returns the services that are associated with it.") + public void mucTestForDiscoveringMuc() throws Exception { + // This repeats some logic from the `AbstractMultiUserChatIntegrationTest` constructor, but is preserved here + // as an explicit test, because that might not always be true. + List services = ServiceDiscoveryManager.getInstanceFor(conOne).findServices(MUCInitialPresence.NAMESPACE, true, false); + assertFalse(services.isEmpty(), "Expected to be able to find MUC services on the domain that '" + conOne.getUser() + "' is connecting to (but could not)."); + } + + /** + * Asserts that a MUC service can have its features discovered. + * + * @throws Exception when errors occur + */ + @SmackIntegrationTest(section = "6.2", quote = + "An entity may wish to discover if a service implements the Multi-User Chat protocol; in order to do so, it " + + "sends a service discovery information (\"disco#info\") query to the MUC service's JID. The service MUST " + + "return its identity and the features it supports.") public void mucTestForDiscoveringFeatures() throws Exception { - DiscoverInfo info = mucManagerOne.getMucServiceDiscoInfo(mucManagerOne.getMucServiceDomains().get(0)); - assertTrue(info.getIdentities().size() > 0); - assertTrue(info.getFeatures().size() > 0); + DiscoverInfo info = ServiceDiscoveryManager.getInstanceFor(conOne).discoverInfo(mucService); + assertFalse(info.getIdentities().isEmpty(), "Expected the service discovery information for service " + mucService + " to include identities (but it did not)."); + assertFalse(info.getFeatures().isEmpty(), "Expected the service discovery information for service " + mucService + " to include features (but it did not)."); } /** * Asserts that a MUC Service lists its public rooms. * - *

    From XEP-0045 § 6.3:

    - *
    - * The service discovery items ("disco#items") protocol enables an entity to query a service for a list of - * associated items, which in the case of a chat service would consist of the specific chat rooms hosted by the - * service. The service SHOULD return a full list of the public rooms it hosts (i.e., not return any rooms that - * are hidden). - *
    - * * @throws Exception when errors occur */ - @SmackIntegrationTest + @SmackIntegrationTest(section = "6.3", quote = + "The service discovery items (\"disco#items\") protocol enables an entity to query a service for a list of " + + "associated items, which in the case of a chat service would consist of the specific chat rooms hosted by the" + + "service. The service SHOULD return a full list of the public rooms it hosts (i.e., not return any rooms that" + + "are hidden).") public void mucTestForDiscoveringRooms() throws Exception { EntityBareJid mucAddressPublic = getRandomRoom("smack-inttest-publicroom"); MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddressPublic); @@ -97,22 +114,18 @@ public class MultiUserChatEntityIntegrationTest extends AbstractMultiUserChatInt tryDestroy(mucAsSeenByTwo); } - assertTrue(rooms.containsKey(mucAddressPublic)); - assertFalse(rooms.containsKey(mucAddressHidden)); + assertTrue(rooms.containsKey(mucAddressPublic), "Expected the disco response from " + mucService + " to include the public room " + mucAddressPublic + " (but it did not)."); + assertFalse(rooms.containsKey(mucAddressHidden), "Expected the disco response from " + mucService + " to not include the hidden room " + mucAddressHidden + " (but it did)."); } /** * Asserts that a MUC Service returns disco info for a room. * - *

    From XEP-0045 § 6.4:

    - *
    - * Using the disco#info protocol, an entity may also query a specific chat room for more detailed information - * about the room....The room MUST return its identity and SHOULD return the features it supports - *
    - * * @throws Exception when errors occur */ - @SmackIntegrationTest + @SmackIntegrationTest(section = "6.4", quote = + "Using the disco#info protocol, an entity may also query a specific chat room for more detailed information " + + "about the room....The room MUST return its identity and SHOULD return the features it supports") public void mucTestForDiscoveringRoomInfo() throws Exception { EntityBareJid mucAddress = getRandomRoom("smack-inttest-discoinfo"); MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); @@ -126,23 +139,20 @@ public class MultiUserChatEntityIntegrationTest extends AbstractMultiUserChatInt tryDestroy(mucAsSeenByOne); } - assertTrue(discoInfo.getIdentities().size() > 0); - assertTrue(discoInfo.getFeatures().size() > 0); + assertFalse(discoInfo.getIdentities().isEmpty(), "Expected the service discovery information for room " + mucAddress + " to include identities (but it did not)."); + assertFalse(discoInfo.getFeatures().isEmpty(), "Expected the service discovery information for room " + mucAddress + " to include features (but it did not)."); + assertTrue(discoInfo.getFeatures().stream().anyMatch(feature -> MultiUserChatConstants.NAMESPACE.equals(feature.getVar())), "Expected the service discovery information for room " + mucAddress + " to include the '" + MultiUserChatConstants.NAMESPACE + "' feature (but it did not)."); } /** * Asserts that a MUC Service returns disco info for a room's items. * - *

    From XEP-0045 § 6.5:

    - *
    - * An entity MAY also query a specific chat room for its associated items. An implementation MAY return a list - * of existing occupants if that information is publicly available, or return no list at all if this information is - * kept private. - *
    - * * @throws Exception when errors occur */ - @SmackIntegrationTest + @SmackIntegrationTest(section = "6.5", quote = + "An entity MAY also query a specific chat room for its associated items. An implementation MAY return a list " + + "of existing occupants if that information is publicly available, or return no list at all if this " + + "information is kept private.") public void mucTestForDiscoveringRoomItems() throws Exception { EntityBareJid mucAddress = getRandomRoom("smack-inttest-discoitems"); MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); @@ -155,22 +165,18 @@ public class MultiUserChatEntityIntegrationTest extends AbstractMultiUserChatInt tryDestroy(mucAsSeenByOne); } - assertEquals(1, roomItems.getItems().size()); + assertEquals(1, roomItems.getItems().size(), "Unexpected amount of disco items for " + mucAddress); } /** * Asserts that a non-occupant receives a Bad Request error when attempting to query an occupant by their * occupant JID. * - *

    From XEP-0045 § 6.6:

    - *
    - * If a non-occupant attempts to send a disco request to an address of the form <room@service/nick>, a MUC service - * MUST return a <bad-request/> error - *
    - * * @throws Exception when errors occur */ - @SmackIntegrationTest + @SmackIntegrationTest(section = "6.6", quote = + "If a non-occupant attempts to send a disco request to an address of the form , a MUC " + + "service MUST return a error") public void mucTestForRejectingDiscoOnRoomOccupantByNonOccupant() throws Exception { EntityBareJid mucAddress = getRandomRoom("smack-inttest-discoitems"); MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); @@ -186,7 +192,8 @@ public class MultiUserChatEntityIntegrationTest extends AbstractMultiUserChatInt XMPPException.XMPPErrorException xe; try { xe = assertThrows(XMPPException.XMPPErrorException.class, - () -> ServiceDiscoveryManager.getInstanceFor(conTwo).discoverItems(mucAsSeenByOneUserJid)); + () -> ServiceDiscoveryManager.getInstanceFor(conTwo).discoverItems(mucAsSeenByOneUserJid), + "Expected an XMPP error when " + conTwo.getUser() + " was trying to discover items of " + mucAsSeenByOneUserJid); } finally { tryDestroy(mucAsSeenByOne); } @@ -200,6 +207,7 @@ public class MultiUserChatEntityIntegrationTest extends AbstractMultiUserChatInt expectedCondition = StanzaError.Condition.not_acceptable; break; } - assertEquals(xe.getStanzaError().getCondition(), expectedCondition); + assertEquals(expectedCondition, xe.getStanzaError().getCondition(), + "Unexpected error condition in error returned when " + conTwo.getUser() + " was trying to discover items of " + mucAsSeenByOneUserJid); } } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatIntegrationTest.java index 36fa47815..be59229c9 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2020 Florian Schmaus + * Copyright 2015-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. @@ -17,100 +17,44 @@ package org.jivesoftware.smackx.muc; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertThrows; +import java.util.Set; import java.util.concurrent.TimeoutException; import org.jivesoftware.smack.MessageListener; import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.SmackException.NoResponseException; +import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.XMPPException.XMPPErrorException; import org.jivesoftware.smack.packet.Message; -import org.jivesoftware.smack.packet.Presence; - -import org.jivesoftware.smackx.muc.packet.MUCUser; +import org.jivesoftware.smack.packet.StanzaError; +import org.jivesoftware.smackx.muc.MultiUserChatException.MissingMucCreationAcknowledgeException; +import org.jivesoftware.smackx.muc.MultiUserChatException.MucAlreadyJoinedException; +import org.jivesoftware.smackx.muc.MultiUserChatException.MucConfigurationNotSupportedException; +import org.jivesoftware.smackx.muc.MultiUserChatException.NotAMucServiceException; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; import org.igniterealtime.smack.inttest.TestNotPossibleException; import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; -import org.igniterealtime.smack.inttest.util.ResultSyncPoint; +import org.igniterealtime.smack.inttest.annotations.SpecificationReference; import org.igniterealtime.smack.inttest.util.SimpleResultSyncPoint; import org.jxmpp.jid.EntityBareJid; import org.jxmpp.jid.parts.Resourcepart; +import org.jxmpp.stringprep.XmppStringprepException; +@SpecificationReference(document = "XEP-0045", version = "1.34.6") public class MultiUserChatIntegrationTest extends AbstractMultiUserChatIntegrationTest { public MultiUserChatIntegrationTest(SmackIntegrationTestEnvironment environment) throws SmackException.NoResponseException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, - InterruptedException, TestNotPossibleException { + InterruptedException, TestNotPossibleException, MucAlreadyJoinedException, MissingMucCreationAcknowledgeException, NotAMucServiceException, XmppStringprepException { super(environment); } - /** - * Asserts that when a user joins a room, they are themselves included on the list of users notified (self-presence). - * - *

    From XEP-0045 § 7.2.2:

    - *
    - * ...the service MUST also send presence from the new participant's occupant JID to the full JIDs of all the - * occupants (including the new occupant) - *
    - * - * @throws Exception when errors occur - */ - @SmackIntegrationTest - public void mucJoinTest() throws Exception { - EntityBareJid mucAddress = getRandomRoom("smack-inttest-join"); - - MultiUserChat muc = mucManagerOne.getMultiUserChat(mucAddress); - try { - Presence reflectedJoinPresence = muc.join(Resourcepart.from("nick-one")); - - MUCUser mucUser = MUCUser.from(reflectedJoinPresence); - - assertNotNull(mucUser); - assertTrue(mucUser.getStatus().contains(MUCUser.Status.PRESENCE_TO_SELF_110)); - assertEquals(mucAddress + "/nick-one", reflectedJoinPresence.getFrom().toString()); - assertEquals(conOne.getUser().asEntityFullJidIfPossible().toString(), reflectedJoinPresence.getTo().toString()); - } finally { - tryDestroy(muc); - } - } - - /** - * Asserts that when a user leaves a room, they are themselves included on the list of users notified (self-presence). - * - *

    From XEP-0045 § 7.14:

    - *
    - * The service MUST then send a presence stanzas of type "unavailable" from the departing user's occupant JID to - * the departing occupant's full JIDs, including a status code of "110" to indicate that this notification is - * "self-presence" - *
    - * - * @throws Exception when errors occur - */ - @SmackIntegrationTest - public void mucLeaveTest() throws Exception { - EntityBareJid mucAddress = getRandomRoom("smack-inttest-leave"); - - MultiUserChat muc = mucManagerOne.getMultiUserChat(mucAddress); - try { - muc.join(Resourcepart.from("nick-one")); - - Presence reflectedLeavePresence = muc.leave(); - - MUCUser mucUser = MUCUser.from(reflectedLeavePresence); - assertNotNull(mucUser); - - assertTrue(mucUser.getStatus().contains(MUCUser.Status.PRESENCE_TO_SELF_110)); - assertEquals(mucAddress + "/nick-one", reflectedLeavePresence.getFrom().toString()); - assertEquals(conOne.getUser().asEntityFullJidIfPossible().toString(), reflectedLeavePresence.getTo().toString()); - } finally { - muc.join(Resourcepart.from("nick-one")); // We need to be in the room to destroy the room - tryDestroy(muc); - } - } - @SmackIntegrationTest public void mucTest() throws Exception { EntityBareJid mucAddress = getRandomRoom("smack-inttest-message"); @@ -119,14 +63,14 @@ public class MultiUserChatIntegrationTest extends AbstractMultiUserChatIntegrati MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); final String mucMessage = "Smack Integration Test MUC Test Message " + randomString; - final ResultSyncPoint resultSyncPoint = new ResultSyncPoint<>(); + final SimpleResultSyncPoint resultSyncPoint = new SimpleResultSyncPoint(); mucAsSeenByTwo.addMessageListener(new MessageListener() { @Override public void processMessage(Message message) { String body = message.getBody(); if (mucMessage.equals(body)) { - resultSyncPoint.signal(body); + resultSyncPoint.signal(); } } }); @@ -134,63 +78,197 @@ public class MultiUserChatIntegrationTest extends AbstractMultiUserChatIntegrati createMuc(mucAsSeenByOne, "one-" + randomString); mucAsSeenByTwo.join(Resourcepart.from("two-" + randomString)); mucAsSeenByOne.sendMessage(mucMessage); + try { - resultSyncPoint.waitForResult(timeout); - } catch (TimeoutException e) { - throw new AssertionError("Failed to receive presence", e); + assertResult(resultSyncPoint, "Expected " + conTwo.getUser() + " to receive message that was sent by " + conOne.getUser() + " in room " + mucAddress + " (but it did not)."); } finally { tryDestroy(mucAsSeenByOne); } } - /** - * Asserts that a user is notified when a room is destroyed - * - *

    From XEP-0045 § 10.9:

    - *
    - * A room owner MUST be able to destroy a room, especially if the room is persistent... The room removes all users from the room... and destroys the room - *
    + /** + * Asserts that an owner is notified of room destruction when they destroy a room. * * @throws TimeoutException when roomDestroyed event doesn't get fired * @throws Exception when other errors occur */ - @SmackIntegrationTest - public void mucDestroyTest() throws TimeoutException, Exception { + @SmackIntegrationTest(section = "10.9", quote = + "A room owner MUST be able to destroy a room, especially if the room is persistent... The room removes all " + + "users from the room... and destroys the room") + public void mucDestroyOwnerTest() throws TimeoutException, Exception { - EntityBareJid mucAddress = getRandomRoom("smack-inttest-destroy"); + EntityBareJid mucAddress = getRandomRoom("smack-inttest-destroy-owner"); MultiUserChat muc = mucManagerOne.getMultiUserChat(mucAddress); - muc.join(Resourcepart.from("nick-one")); + createMuc(muc, Resourcepart.from("one-" + randomString)); + + // These would be a test implementation bug, not assertion failure. + if (!mucManagerOne.getJoinedRooms().contains(mucAddress)) { + tryDestroy(muc); + throw new IllegalStateException("Expected user to have joined a room '" + mucAddress + "' (but does not appear to have done so)."); + } final SimpleResultSyncPoint mucDestroyed = new SimpleResultSyncPoint(); - @SuppressWarnings("deprecation") - DefaultUserStatusListener userStatusListener = new DefaultUserStatusListener() { + UserStatusListener userStatusListener = new UserStatusListener() { @Override - public void roomDestroyed(MultiUserChat alternateMUC, String reason) { + public void roomDestroyed(MultiUserChat alternateMUC, String password, String reason) { mucDestroyed.signal(); } }; muc.addUserStatusListener(userStatusListener); - assertEquals(1, mucManagerOne.getJoinedRooms().size()); - assertEquals(1, muc.getOccupantsCount()); - assertNotNull(muc.getNickname()); - try { muc.destroy("Dummy reason", null); - mucDestroyed.waitForResult(timeout); + assertResult(mucDestroyed, "Expected " + conOne.getUser() + " to be notified of destruction of room " + mucAddress + " (but was not)."); } finally { muc.removeUserStatusListener(userStatusListener); } - assertEquals(0, mucManagerOne.getJoinedRooms().size()); - assertEquals(0, muc.getOccupantsCount()); + Set joinedRooms = mucManagerOne.getJoinedRooms(); + assertFalse(muc.isJoined(), "Expected " + conOne.getUser() + " to no longer be in room " + mucAddress + " after it was destroyed, but it is still in."); + assertEquals(0, joinedRooms.size(), "Expected " + conOne.getUser() + " to no longer be in any rooms after " + mucAddress + " was destroyed. But it is still in " + joinedRooms); + assertEquals(0, muc.getOccupantsCount(), "Expected room " + mucAddress + " to no longer have any occupants after it was destroyed (but it has)."); assertNull(muc.getNickname()); } + /** + * Asserts that an occupant of a room is notified when a room is destroyed. + * + * @throws TimeoutException when roomDestroyed event doesn't get fired + * @throws Exception when other errors occur + */ + @SmackIntegrationTest(section = "10.9", quote = + "A room owner MUST be able to destroy a room, especially if the room is persistent... The room removes all " + + "users from the room... and destroys the room") + public void mucDestroyTestOccupant() throws TimeoutException, Exception { + + EntityBareJid mucAddress = getRandomRoom("smack-inttest-destroy-occupant"); + + MultiUserChat mucAsSeenByOwner = mucManagerOne.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByParticipant = mucManagerTwo.getMultiUserChat(mucAddress); + createMuc(mucAsSeenByOwner, Resourcepart.from("one-" + randomString)); + + // These would be a test implementation bug, not assertion failure. + mucAsSeenByParticipant.join(Resourcepart.from("two-" + randomString)); + if (!mucManagerTwo.getJoinedRooms().contains(mucAddress)) { + tryDestroy(mucAsSeenByOwner); + throw new IllegalStateException("Expected user to have joined a room '" + mucAddress + "' (but does not appear to have done so)."); + } + final SimpleResultSyncPoint mucDestroyed = new SimpleResultSyncPoint(); + + UserStatusListener userStatusListener = new UserStatusListener() { + @Override + public void roomDestroyed(MultiUserChat alternateMUC, String password, String reason) { + mucDestroyed.signal(); + } + }; + + mucAsSeenByParticipant.addUserStatusListener(userStatusListener); + + try { + mucAsSeenByOwner.destroy("Dummy reason", null); + assertResult(mucDestroyed, "Expected " + conTwo.getUser() + " to be notified of destruction of room " + mucAddress + " (but was not)."); + } finally { + mucAsSeenByParticipant.removeUserStatusListener(userStatusListener); + } + + Set joinedRooms = mucManagerTwo.getJoinedRooms(); + assertFalse(mucAsSeenByParticipant.isJoined(), "Expected " + conTwo.getUser() + " to no longer be in room " + mucAddress + " after it was destroyed, but it is still in."); + assertEquals(0, joinedRooms.size(), "Expected " + conTwo.getUser() + " to no longer be in any rooms after " + mucAddress + " was destroyed. But it is still in " + joinedRooms); + assertEquals(0, mucAsSeenByParticipant.getOccupantsCount(), "Expected room " + mucAddress + " to no longer have any occupants after it was destroyed (but it has)."); + assertNull(mucAsSeenByParticipant.getNickname()); + } + + @SmackIntegrationTest + public void mucNameChangeTest() + throws XmppStringprepException, MucAlreadyJoinedException, MissingMucCreationAcknowledgeException, + NotAMucServiceException, NoResponseException, XMPPErrorException, NotConnectedException, + InterruptedException, MucConfigurationNotSupportedException { + EntityBareJid mucAddress = getRandomRoom("smack-inttest-muc-name-change"); + + MultiUserChat muc = mucManagerOne.getMultiUserChat(mucAddress); + createMuc(muc, Resourcepart.from("one-" + randomString)); + + final String newRoomName = "New Room Name (" + randomString + ")"; + + try { + muc.getConfigFormManager() + .setRoomName(newRoomName) + .submitConfigurationForm(); + + MultiUserChatManager mucManager = MultiUserChatManager.getInstanceFor(conTwo); + RoomInfo roomInfo = mucManager.getRoomInfo(muc.getRoom()); + assertEquals(newRoomName, roomInfo.getName()); + } finally { + tryDestroy(muc); + } + } + + @SmackIntegrationTest(section = "8.1", quote = "modify the subject [...] MUST be denied if the of the 'from' address of the request does not match " + + "the bare JID portion of one of the moderators; in this case, the service MUST return a error.") + public void mucTestVisitorNotAllowedToChangeSubject() throws XmppStringprepException, MucAlreadyJoinedException, + MissingMucCreationAcknowledgeException, NotAMucServiceException, NoResponseException, + XMPPErrorException, NotConnectedException, InterruptedException, TestNotPossibleException { + final EntityBareJid mucAddress = getRandomRoom("smack-inttest-visitor-change-subject"); + final MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); + final MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); + + final Resourcepart nicknameOne = Resourcepart.from("one-" + randomString); + final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); + + createMuc(mucAsSeenByOne, nicknameOne); + try { + MucConfigFormManager configFormManager = mucAsSeenByOne.getConfigFormManager(); + if (configFormManager.occupantsAreAllowedToChangeSubject()) { + configFormManager.disallowOccupantsToChangeSubject().submitConfigurationForm(); + } + + mucAsSeenByTwo.join(nicknameTwo); + + final XMPPException.XMPPErrorException e = assertThrows(XMPPException.XMPPErrorException.class, () -> { + mucAsSeenByTwo.changeSubject("Test Subject Change"); + }, "Expected an error after '" + conTwo.getUser() + + "' (that is not a moderator) tried to change the subject of room '" + mucAddress + + "' (but none occurred)."); + assertEquals(StanzaError.Condition.forbidden, e.getStanzaError().getCondition(), + "Unexpected error condition in the (expected) error that was returned to '" + + conTwo.getUser() + "' after it tried to change to subject of room '" + + mucAddress + "' while not being a moderator."); + } catch (MucConfigurationNotSupportedException e) { + throw new TestNotPossibleException(e); + } finally { + tryDestroy(mucAsSeenByOne); + } + } + + @SmackIntegrationTest + public void mucTestChangeRoomName() throws XmppStringprepException, MucAlreadyJoinedException, + MissingMucCreationAcknowledgeException, NotAMucServiceException, NoResponseException, + XMPPErrorException, NotConnectedException, InterruptedException, TestNotPossibleException { + final EntityBareJid mucAddress = getRandomRoom("smack-inttest-change-room-name"); + final MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); + final Resourcepart nicknameOne = Resourcepart.from("one-" + randomString); + + createMuc(mucAsSeenByOne, nicknameOne); + try { + String initialRoomName = "Initial Room Name"; + mucAsSeenByOne.getConfigFormManager().setRoomName(initialRoomName).submitConfigurationForm(); + RoomInfo roomInfo = mucManagerOne.getRoomInfo(mucAddress); + assertEquals(initialRoomName, roomInfo.getName()); + + String newRoomName = "New Room Name"; + mucAsSeenByOne.getConfigFormManager().setRoomName(newRoomName).submitConfigurationForm(); + roomInfo = mucManagerOne.getRoomInfo(mucAddress); + assertEquals(newRoomName, roomInfo.getName()); + } catch (MucConfigurationNotSupportedException e) { + throw new TestNotPossibleException(e); + } finally { + tryDestroy(mucAsSeenByOne); + } + } } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatLowLevelIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatLowLevelIntegrationTest.java index e0f476306..e13384940 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatLowLevelIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatLowLevelIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2020 Florian Schmaus + * Copyright 2015-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. @@ -33,13 +33,16 @@ import org.igniterealtime.smack.inttest.AbstractSmackLowLevelIntegrationTest; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; import org.igniterealtime.smack.inttest.TestNotPossibleException; import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; +import org.igniterealtime.smack.inttest.annotations.SpecificationReference; import org.jxmpp.jid.DomainBareJid; import org.jxmpp.jid.impl.JidCreate; import org.jxmpp.jid.parts.Localpart; import org.jxmpp.jid.parts.Resourcepart; +@SpecificationReference(document = "XEP-0048", version = "1.2") public class MultiUserChatLowLevelIntegrationTest extends AbstractSmackLowLevelIntegrationTest { + @SuppressWarnings("this-escape") public MultiUserChatLowLevelIntegrationTest(SmackIntegrationTestEnvironment environment) throws Exception { super(environment); AbstractXMPPConnection connection = getConnectedConnection(); @@ -66,7 +69,13 @@ public class MultiUserChatLowLevelIntegrationTest extends AbstractSmackLowLevelI final MultiUserChat muc = multiUserChatManager.getMultiUserChat(JidCreate.entityBareFrom( Localpart.from(randomMucName), mucComponent)); - MucCreateConfigFormHandle handle = muc.createOrJoin(mucNickname); + MucCreateConfigFormHandle handle; + try { + handle = muc.createOrJoin(mucNickname); + } catch (XMPPException.XMPPErrorException e) { + AbstractMultiUserChatIntegrationTest.mucCreationDisallowedOrThrow(e); + throw new TestNotPossibleException("MUC service " + mucComponent + " does not allow MUC creation", e); + } if (handle != null) { handle.makeInstant(); } @@ -83,7 +92,7 @@ public class MultiUserChatLowLevelIntegrationTest extends AbstractSmackLowLevelI // So we trigger it manually here. MucBookmarkAutojoinManager.getInstanceFor(connection).autojoinBookmarkedConferences(); - assertTrue(muc.isJoined()); + assertTrue(muc.isJoined(), "Expected " + connection.getUser() + " to automatically join room " + muc.getRoom() + " after a reconnect, but it did not."); // If the test went well, leave the MUC muc.leave(); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatOccupantIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatOccupantIntegrationTest.java new file mode 100644 index 000000000..04ea0253c --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatOccupantIntegrationTest.java @@ -0,0 +1,1118 @@ +/** + * + * Copyright 2015-2024 Florian Schmaus, 2021 Dan Caseley + * + * 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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; + +import org.jivesoftware.smack.PresenceListener; +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.StanzaListener; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.FromMatchesFilter; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Presence; +import org.jivesoftware.smack.packet.Stanza; +import org.jivesoftware.smack.packet.StanzaError; +import org.jivesoftware.smack.sm.predicates.ForEveryMessage; +import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smackx.muc.MultiUserChatException.MissingMucCreationAcknowledgeException; +import org.jivesoftware.smackx.muc.MultiUserChatException.MucAlreadyJoinedException; +import org.jivesoftware.smackx.muc.MultiUserChatException.MucConfigurationNotSupportedException; +import org.jivesoftware.smackx.muc.MultiUserChatException.NotAMucServiceException; +import org.jivesoftware.smackx.muc.packet.MUCItem; +import org.jivesoftware.smackx.muc.packet.MUCUser; + +import org.igniterealtime.smack.inttest.Configuration; +import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; +import org.igniterealtime.smack.inttest.TestNotPossibleException; +import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; +import org.igniterealtime.smack.inttest.annotations.SpecificationReference; +import org.igniterealtime.smack.inttest.util.MultiResultSyncPoint; +import org.igniterealtime.smack.inttest.util.ResultSyncPoint; +import org.igniterealtime.smack.inttest.util.SimpleResultSyncPoint; +import org.jxmpp.jid.EntityBareJid; +import org.jxmpp.jid.impl.JidCreate; +import org.jxmpp.jid.parts.Resourcepart; +import org.jxmpp.stringprep.XmppStringprepException; + +@SpecificationReference(document = "XEP-0045", version = "1.34.6") +public class MultiUserChatOccupantIntegrationTest extends AbstractMultiUserChatIntegrationTest { + + public MultiUserChatOccupantIntegrationTest(SmackIntegrationTestEnvironment environment) + throws SmackException.NoResponseException, XMPPException.XMPPErrorException, + SmackException.NotConnectedException, InterruptedException, TestNotPossibleException, MucAlreadyJoinedException, MissingMucCreationAcknowledgeException, NotAMucServiceException, XmppStringprepException { + super(environment); + } + + /** + * Asserts that when a user joins a room, all events are received, and in the correct order. + * + * @throws Exception when errors occur + */ + @SmackIntegrationTest(section = "7.1 & 7.2.2", quote = "" + + "§ 7.1 The order of events involved in joining a room needs to be consistent so that clients can know which events to expect when. After a client sends presence to join a room, the MUC service MUST send it events in the following order: 1. In-room presence from other occupants 2. In-room presence from the joining entity itself (so-called \"self-presence\") 3. Room history (if any) 4. The room subject [...]" + + "§ 7.2.2 This self-presence MUST NOT be sent to the new occupant until the room has sent the presence of all other occupants to the new occupant ... The service MUST first send the complete list of the existing occupants to the new occupant and only then send the new occupant's own presence to the new occupant") + public void mucJoinEventOrderingTest() throws Exception { + // This implementation deliberately does not use Smack's MUC API when trying to collect the order in which + // stanzas arrive. Instead, it joins a chatroom and listens for its stanzas using basic stanza handling. As + // this uses exactly one stanza listener, that's guaranteed to be invoked in order of stanza arrival, which is + // not necessarily the case when using the MUC API. + EntityBareJid mucAddress = getRandomRoom("smack-inttest-eventordering"); + final String mucSubject = "Subject smack-inttest-eventordering " + randomString; + final String mucMessage = "Message smack-inttest-eventordering " + randomString; + + MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); + + final Resourcepart nicknameOne = Resourcepart.from("one-" + randomString); + final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); + + createMuc(mucAsSeenByOne, nicknameOne); + mucAsSeenByOne.changeSubject(mucSubject); // Blocks until confirmed. + + // Send and wait for the message to have been reflected, so that we can be sure it's part of the MUC history. + final SimpleResultSyncPoint messageReflectionSyncPoint = new SimpleResultSyncPoint(); + mucAsSeenByOne.addMessageListener(message -> { + String body = message.getBody(); + if (body == null) return; + + if (body.equals(mucMessage)) { + messageReflectionSyncPoint.signal(); + } + }); + + mucAsSeenByOne.sendMessage(mucMessage); + messageReflectionSyncPoint.waitForResult(timeout); + + final ResultSyncPoint subjectResultSyncPoint = new ResultSyncPoint<>(); + final List results = new ArrayList<>(); + final StanzaListener stanzaListener = stanza -> { + results.add(stanza); + if (stanza instanceof Message && ((Message) stanza).getSubject() != null) { + subjectResultSyncPoint.signal(((Message) stanza).getSubject()); + } + }; + conTwo.addStanzaListener(stanzaListener, FromMatchesFilter.create(mucAddress)); + + try { + mucAsSeenByTwo.join(nicknameTwo); + + subjectResultSyncPoint.waitForResult(timeout); // Wait for subject, as it should be 4th (last) + + assertEquals(4, results.size(), "Unexpected amount of stanzas received by '" + conTwo.getUser() + "' after it joined room '" + mucAddress + "'."); + assertTrue(results.get(0) instanceof Presence, "Expected the first stanza that was received by '" + conTwo.getUser() + "' after it joined room '" + mucAddress + "' to be a presence stanza (but it was not)."); + assertEquals(JidCreate.fullFrom(mucAddress, nicknameOne), results.get(0).getFrom(), "Unexpected 'from' address of the first stanza that was received by '" + conTwo.getUser() + "' after it joined room '" + mucAddress + "'."); + assertTrue(results.get(1) instanceof Presence, "Expected the second stanza that was received by '" + conTwo.getUser() + "' after it joined room '" + mucAddress + "' to be a presence stanza (but it was not)."); + assertEquals(JidCreate.fullFrom(mucAddress, nicknameTwo), results.get(1).getFrom(), "Unexpected 'from' address of the seconds stanza that was received by '" + conTwo.getUser() + "' after it joined room '" + mucAddress + "'."); + assertTrue(results.get(2) instanceof Message, "Expected the third stanza that was received by '" + conTwo.getUser() + "' after it joined room '" + mucAddress + "' to be a message stanza (but it was not)."); + assertEquals(mucMessage, ((Message) results.get(2)).getBody(), "The third stanza that was received by '" + conTwo.getUser() + "' after it joined room '" + mucAddress + "' was expected to be a different stanza."); + assertTrue(results.get(3) instanceof Message, "Expected the fourth stanza that was received by '" + conTwo.getUser() + "' after it joined room '" + mucAddress + "' to be a message stanza (but it was not)."); + assertEquals(mucSubject, ((Message) results.get(3)).getSubject(), "The fourth stanza that was received by '" + conTwo.getUser() + "' after it joined room '" + mucAddress + "' was expected to be a different stanza."); + } finally { + tryDestroy(mucAsSeenByOne); + conTwo.removeStanzaListener(stanzaListener); + } + } + + /** + * Asserts that when a user sends a message to a room without joining, they receive an error and the message is not + * sent to the occupants. + * + *

    From XEP-0045 § 7.2.1:

    + *
    + * In order to participate in the discussions held in a multi-user chat room, a user MUST first become an occupant + * by entering the room + *
    + * + *

    From XEP-0045 § 7.4:

    + *
    + * If the sender is not an occupant of the room, the service SHOULD return a <not-acceptable/> error to the + * sender and SHOULD NOT reflect the message to all occupants + *
    + * + * @throws Exception when errors occur + */ + @SmackIntegrationTest(section = "7.2.1 & 7.4", quote = + "§ 7.2.1: In order to participate in the discussions held in a multi-user chat room, a user MUST first become an occupant by entering the room [...] " + + "§ 7.4: If the sender is not an occupant of the room, the service SHOULD return a error to the sender and SHOULD NOT reflect the message to all occupants") + public void mucSendBeforeJoiningTest() throws Exception { + EntityBareJid mucAddress = getRandomRoom("smack-inttest-send-without-joining"); + + MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); + + createMuc(mucAsSeenByOne, Resourcepart.from("one-" + randomString)); + + ResultSyncPoint errorMessageResultSyncPoint = new ResultSyncPoint<>(); + conTwo.addStanzaListener(packet -> errorMessageResultSyncPoint.signal((Message) packet), ForEveryMessage.INSTANCE); + + ResultSyncPoint distributedMessageResultSyncPoint = new ResultSyncPoint<>(); + mucAsSeenByOne.addMessageListener(distributedMessageResultSyncPoint::signal); + + try { + mucAsSeenByTwo.sendMessage("Message without Joining"); + Message response = assertResult(errorMessageResultSyncPoint, "Expected an error to be returned to '" + conTwo.getUser() + "' after it sent a message to room '" + mucAddress + "' without joining it first (but no error was returned)."); + assertEquals(StanzaError.Condition.not_acceptable, response.getError().getCondition(), "Unexpected error condition in the (expected) error that was returned to '" + conTwo.getUser() + "' after it sent a message to room '" + mucAddress + "' without joining it first."); + assertThrows(TimeoutException.class, () -> distributedMessageResultSyncPoint.waitForResult(1000), "Occupant '" + conOne.getUser() + "' should NOT have seen the message that was sent by '" + conTwo.getUser() + "' to room '" + mucAddress + "' without the sender have joined the room (but the message was observed by the occupant)."); + } finally { + tryDestroy(mucAsSeenByOne); + } + } + + /** + * Asserts that when a user joins a room, they are sent presence information about existing participants and + * themselves that includes role and affiliation information and appropriate status codes. + * + * @throws Exception when errors occur + */ + @SmackIntegrationTest(section = "7.2.2", quote = + "If the service is able to add the user to the room, it MUST send presence from all the existing participants' " + + "occupant JIDs to the new occupant's full JID, including extended presence information about roles in a " + + "single element qualified by the 'http://jabber.org/protocol/muc#user' namespace and containing an " + + " child with the 'role' attribute set to a value of \"moderator\", \"participant\", or \"visitor\", " + + "and with the 'affiliation' attribute set to a value of \"owner\", \"admin\", \"member\", or \"none\" as " + + "appropriate. [...] the \"self-presence\" sent by the room to the new user MUST include a status code of 110 " + + "so that the user knows this presence refers to itself as an occupant [...] The service MUST first send the " + + "complete list of the existing occupants to the new occupant and only then send the new occupant's own " + + "presence to the new occupant.") + public void mucJoinPresenceInformationTest() throws Exception { + EntityBareJid mucAddress = getRandomRoom("smack-inttest-presenceinfo"); + + MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByThree = mucManagerThree.getMultiUserChat(mucAddress); + + final Resourcepart nicknameOne = Resourcepart.from("one-" + randomString); + final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); + final Resourcepart nicknameThree = Resourcepart.from("three-" + randomString); + + createMuc(mucAsSeenByOne, nicknameOne); + SimpleResultSyncPoint oneSeesTwo = new SimpleResultSyncPoint(); + mucAsSeenByOne.addParticipantListener(presence -> { + if (nicknameTwo.equals(presence.getFrom().getResourceOrEmpty())) { + oneSeesTwo.signal(); + } + }); + mucAsSeenByTwo.join(nicknameTwo); + oneSeesTwo.waitForResult(timeout); + mucAsSeenByOne.grantModerator(nicknameTwo); + + List results = new ArrayList<>(); + mucAsSeenByThree.addParticipantListener(results::add); + + try { + // Will block until all self-presence is received, prior to which all others presences will have been received. + mucAsSeenByThree.join(nicknameThree); + + assertEquals(3, results.size(), "Unexpected amount of occupants seen by '" + conThree + "' in room '" + mucAddress + "' after joining."); // The 3rd will be self-presence. + assertNotNull(MUCUser.from(results.get(0)), "Expected to be able to parse a MUC occupant from '" + results.get(0) + "', but could not. Its syntax is likely incorrect."); // Smack implementation guarantees the "x" element and muc#user namespace + + // The order of all but the last presence (which should be the self-presence) is unpredictable. + MUCItem mucItemSelf = MUCUser.from(results.get(2)).getItem(); + Set others = new HashSet<>(); + others.add(MUCUser.from(results.get(0)).getItem()); + others.add(MUCUser.from(results.get(1)).getItem()); + + assertEquals(MUCAffiliation.none, mucItemSelf.getAffiliation(), "Unexpected MUC affiliation in reflected self-presence of '" + conThree.getUser() + "' joining room '" + mucAddress + "'."); + assertEquals(1, others.stream().filter(item -> MUCAffiliation.owner.equals(item.getAffiliation())).count(), "Unexpected amount of other occupants in room '" + mucAddress + "' (as observed by '" + conThree.getUser() + "') that have the 'owner' affiliation."); + assertEquals(1, others.stream().filter(item -> MUCAffiliation.none.equals(item.getAffiliation())).count(), "Unexpected amount of other occupants in room '" + mucAddress + "' (as observed by '" + conThree.getUser() + "') that have no affiliation."); + + assertEquals(MUCRole.participant, mucItemSelf.getRole(), "Unexpected MUC role in reflected self-presence of '" + conThree.getUser() + "' joining room '" + mucAddress + "'."); + assertEquals(2, others.stream().filter(item -> MUCRole.moderator.equals(item.getRole())).count(), "Unexpected amount of other occupants in room '" + mucAddress + "' (as observed by '" + conThree.getUser() + "') that have the 'moderator' role."); + + assertTrue(MUCUser.from(results.get(2)).getStatus().contains(MUCUser.Status.PRESENCE_TO_SELF_110), "Expected to find status '" + MUCUser.Status.PRESENCE_TO_SELF_110 + "' in reflected self-presence of '" + conThree.getUser() + "' joining room '" + mucAddress + "' (but did not)."); + } finally { + tryDestroy(mucAsSeenByOne); + } + } + + /** + * Asserts that when a user joins a room, all users are sent presence information about the new participant. + * + * @throws Exception when errors occur + */ + @SmackIntegrationTest(section = "7.2.2", quote = + "the service MUST also send presence from the new participant's occupant JID to the full JIDs of all the " + + "occupants (including the new occupant)") + public void mucJoinPresenceBroadcastTest() throws Exception { + EntityBareJid mucAddress = getRandomRoom("smack-inttest-presenceinfo"); + + MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByThree = mucManagerThree.getMultiUserChat(mucAddress); + + final Resourcepart nicknameOne = Resourcepart.from("one-" + randomString); + final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); + final Resourcepart nicknameThree = Resourcepart.from("three-" + randomString); + + createMuc(mucAsSeenByOne, nicknameOne); + mucAsSeenByTwo.join(nicknameTwo); + + final MultiResultSyncPoint syncPoint = new MultiResultSyncPoint<>(2); + + mucAsSeenByOne.addParticipantListener(presence -> { + if (nicknameThree.equals(presence.getFrom().getResourceOrEmpty())) { + syncPoint.signal(presence); + } + }); + + mucAsSeenByTwo.addParticipantListener(presence -> { + if (nicknameThree.equals(presence.getFrom().getResourceOrEmpty())) { + syncPoint.signal(presence); + } + }); + + try { + mucAsSeenByThree.join(nicknameThree); + + List results = assertResult(syncPoint, "Expected all occupants of room '" + mucAddress + "' to be notified of '" + conThree.getUser() + "' using nickname '" + nicknameThree + "' joining the room (but one or more did not get notified)"); + assertTrue(results.stream().allMatch( + result -> JidCreate.fullFrom(mucAddress, nicknameThree).equals(result.getFrom())), + "Expected all occupants of room '" + mucAddress + "' to be notified of '" + conThree.getUser() + "' using nickname '" + nicknameThree + "' joining the room (but one or more got notified for a different user)."); + assertTrue(results.stream().anyMatch( + result -> result.getTo().equals(conOne.getUser().asEntityFullJidIfPossible())), + "Expected '" + conOne.getUser().asEntityFullJidIfPossible() + "' to be notified of '" + conThree.getUser() + "' joining room '" + mucAddress + "' (but did not)"); + assertTrue(results.stream().anyMatch( + result -> result.getTo().equals(conTwo.getUser().asEntityFullJidIfPossible())), + "Expected '" + conTwo.getUser().asEntityFullJidIfPossible() + "' to be notified of '" + conThree.getUser() + "' joining room '" + mucAddress + "' (but did not)"); + } finally { + tryDestroy(mucAsSeenByOne); + } + } + + /** + * Asserts that when a user enters a non-anonymous room, the presence notifications contain extended presence + * information. + * + * @throws Exception when errors occur + */ + @SmackIntegrationTest(section = "7.2.3", quote = + "If the room is non-anonymous, the service MUST send the new occupant's full JID to all occupants using " + + "extended presence information in an element qualified by the 'http://jabber.org/protocol/muc#user' " + + "namespace and containing an child with a 'jid' attribute specifying the occupant's full JID. [...]" + + "If the user is entering a room that is non-anonymous (i.e., which informs all occupants of each occupant's " + + "full JID as shown above), the service MUST warn the user by including a status code of \"100\" in the " + + "initial presence that the room sends to the new occupant.") + public void mucJoinNonAnonymousRoomTest() throws Exception { + EntityBareJid mucAddress = getRandomRoom("smack-inttest-joinnonanonymousroom"); + + MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); + + final Resourcepart nicknameOne = Resourcepart.from("one-" + randomString); + final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); + + createNonAnonymousMuc(mucAsSeenByOne, nicknameOne); + + final ResultSyncPoint participantOneSyncPoint = new ResultSyncPoint<>(); + mucAsSeenByOne.addParticipantListener(presence -> { + if (nicknameTwo.equals(presence.getFrom().getResourceOrEmpty())) { + participantOneSyncPoint.signal(presence); + } + }); + + final ResultSyncPoint participantTwoSyncPoint = new ResultSyncPoint<>(); + mucAsSeenByTwo.addParticipantListener(presence -> { + if (nicknameTwo.equals(presence.getFrom().getResourceOrEmpty())) { + participantTwoSyncPoint.signal(presence); + } + }); + + try { + mucAsSeenByTwo.join(nicknameTwo); + Presence presenceReceivedByOne = assertResult(participantOneSyncPoint, "Expected '" + conOne.getUser() + "' to receive a presence stanza after '" + conTwo.getUser() + "' joined room '" + mucAddress + "' (but did not)."); + Presence presenceReceivedByTwo = assertResult(participantTwoSyncPoint, "Expected '" + conTwo.getUser() + "' to receive a presence stanza after they themselves joined room '" + mucAddress + "' (but did not)."); + + // Check the presence received by participant one for inclusion of full jid of participant two + MUCUser announcedParticipantTwoUser = MUCUser.from(presenceReceivedByOne); + assertNotNull(announcedParticipantTwoUser, "Expected to be able to parse a MUC occupant from '" + presenceReceivedByOne + "', but could not. Its syntax is likely incorrect."); // Smack implementation guarantees the "x" element and muc#user namespace + assertNotNull(announcedParticipantTwoUser.getItem(), "Expected to be able to parse a MUC occupant item from '" + presenceReceivedByOne + "', but could not. Its syntax is likely incorrect."); + assertEquals(conTwo.getUser().asEntityFullJidOrThrow(), announcedParticipantTwoUser.getItem().getJid(), "Expected extended presence information received by '" + conOne.getUser() + "' after '" + conTwo.getUser() + "' joined room '" + mucAddress + "' to include their full JID."); + + // Check the presence received by participant two for inclusion of status 100 + assertTrue(MUCUser.from(presenceReceivedByTwo).getStatus().stream().anyMatch(status -> 100 == status.getCode()), + "Expected to find status '100' in reflected self-presence of '" + conTwo.getUser() + "' joining room '" + mucAddress + "' (but did not)."); + + } finally { + tryDestroy(mucAsSeenByOne); + } + } + + /** + * Asserts that when a user enters a semi-anonymous room, the presence notifications received by occupants that + * are not a moderator does not contain extended presence information. + * + * @throws Exception when errors occur + */ + @SmackIntegrationTest(section = "7.2.4", quote = + "If the room is semi-anonymous, the service MUST send presence from the new occupant to all occupants as " + + "specified above (i.e., unless the room is configured to not broadcast presence from new occupants below a " + + "certain affiliation level as controlled by the \"muc#roomconfig_presencebroadcast\" room configuration " + + "option), but MUST include the new occupant's full JID only in the presence notifications it sends to " + + "occupants with a role of \"moderator\" and not to non-moderator occupants.") + public void mucJoinSemiAnonymousRoomReceivedByNonModeratorTest() throws Exception { + EntityBareJid mucAddress = getRandomRoom("smack-inttest-seminanonymous-by-non-moderator"); + + MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByThree = mucManagerThree.getMultiUserChat(mucAddress); + + final Resourcepart nicknameOne = Resourcepart.from("one-" + randomString); + final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); + final Resourcepart nicknameThree = Resourcepart.from("three-" + randomString); + + createSemiAnonymousMuc(mucAsSeenByOne, nicknameOne); + + mucAsSeenByTwo.join(nicknameTwo); + + // First pass: participant two is not a moderator yet + final ResultSyncPoint participantTwoSyncPoint = new ResultSyncPoint<>(); + mucAsSeenByTwo.addParticipantListener(presence -> { + if (nicknameThree.equals(presence.getFrom().getResourceOrEmpty())) { + participantTwoSyncPoint.signal(presence); + } + }); + + try { + mucAsSeenByThree.join(nicknameThree); + Presence presenceReceivedByTwo = assertResult(participantTwoSyncPoint, "Expected '" + conTwo.getUser() + "' to receive presence when '" + conThree.getUser() + "' joined room '" + mucAddress + "' (but did not)."); + + // Check the presence received by participant two for exclusion of full jid of participant three + assertNull(MUCUser.from(presenceReceivedByTwo).getItem().getJid(), "Did not expect '" + conTwo.getUser() + "' (who is not a moderator at this stage) to receive the full JID of '" + conThree.getUser() + "' when they joined room '" + mucAddress + "' (but they did)."); + } finally { + tryDestroy(mucAsSeenByOne); + } + } + + /** + * Asserts that when a user enters a semi-anonymous room, the presence notifications contain extended presence + * information when sent to moderators. + * + * @throws Exception when errors occur + */ + @SmackIntegrationTest(section = "7.2.4", quote = + "If the room is semi-anonymous, the service MUST send presence from the new occupant to all occupants as " + + "specified above (i.e., unless the room is configured to not broadcast presence from new occupants below a " + + "certain affiliation level as controlled by the \"muc#roomconfig_presencebroadcast\" room configuration " + + "option), but MUST include the new occupant's full JID only in the presence notifications it sends to " + + "occupants with a role of \"moderator\" and not to non-moderator occupants.") + public void mucJoinSemiAnonymousRoomReceivedByModeratorTest() throws Exception { + EntityBareJid mucAddress = getRandomRoom("smack-inttest-seminanonymous-by-moderator"); + + MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByThree = mucManagerThree.getMultiUserChat(mucAddress); + + final Resourcepart nicknameOne = Resourcepart.from("one-" + randomString); + final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); + final Resourcepart nicknameThree = Resourcepart.from("three-" + randomString); + + // Second pass: participant two is now a moderator + createSemiAnonymousMuc(mucAsSeenByOne, nicknameOne); + mucAsSeenByTwo.join(nicknameTwo); + + mucAsSeenByOne.grantModerator(nicknameTwo); + final ResultSyncPoint participantTwoSyncPoint = new ResultSyncPoint<>(); + mucAsSeenByTwo.addParticipantListener(presence -> { + if (nicknameThree.equals(presence.getFrom().getResourceOrEmpty())) { + participantTwoSyncPoint.signal(presence); + } + }); + + try { + mucAsSeenByThree.join(nicknameThree); + Presence presenceReceivedByTwo = assertResult(participantTwoSyncPoint, "Expected '" + conTwo.getUser() + "' to receive presence when '" + conThree.getUser() + "' joined room '" + mucAddress + "' (but did not)."); + + // Check the presence received by participant two for inclusion of full jid of participant three + MUCUser announcedParticipantThreeUser = MUCUser.from(presenceReceivedByTwo); + assertEquals(conThree.getUser().asEntityFullJidOrThrow(), announcedParticipantThreeUser.getItem().getJid(), "Expected '" + conTwo.getUser() + "' (who is a moderator at this stage) to receive the full JID of '" + conThree.getUser() + "' when they joined room '" + mucAddress + "' (but they did not)."); + } finally { + tryDestroy(mucAsSeenByOne); + } + } + + /** + * Asserts that a user can not enter a password-protected room without supplying a password. + * + * @throws Exception when errors occur + */ + @SmackIntegrationTest(section = "7.2.5", quote = + "If the room requires a password and the user did not supply one (or the password provided is incorrect), the " + + "service MUST deny access to the room and inform the user that they are unauthorized; this is done by returning " + + "a presence stanza of type \"error\" specifying a error.") + public void mucJoinPasswordProtectedWithoutPasswordRoomTest() throws Exception { + EntityBareJid mucAddress = getRandomRoom("smack-inttest-enterpasswordprotectedroom"); + + MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); + + final Resourcepart nicknameOne = Resourcepart.from("one-" + randomString); + final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); + + final String correctPassword = StringUtils.insecureRandomString(8); + + createPasswordProtectedMuc(mucAsSeenByOne, nicknameOne, correctPassword); + + try { + // First try: no password + XMPPException.XMPPErrorException noPasswordErrorException = assertThrows( + XMPPException.XMPPErrorException.class, () -> mucAsSeenByTwo.join(nicknameTwo), + "Expected an error to be returned when '" + conTwo.getUser() + "' attempted to join password-protected room '" + mucAddress + "' without a password."); + assertNotNull(noPasswordErrorException.getStanzaError(), + "Expected an error to be returned when '" + conTwo.getUser() + "' attempted to join password-protected room '" + mucAddress + "' without a password."); + assertEquals(StanzaError.Condition.not_authorized, noPasswordErrorException.getStanzaError().getCondition(), + "Unexpected condition in the (expected) error that was returned when '" + conTwo.getUser() + "' attempted to join password-protected room '" + mucAddress + "' without a password."); + } finally { + mucAsSeenByOne.destroy(); + } + } + + /** + * Asserts that a user can not enter a password-protected room without supplying the correct password. + * + * @throws Exception when errors occur + */ + @SmackIntegrationTest(section = "7.2.5", quote = + "If the room requires a password and the user did not supply one (or the password provided is incorrect), the " + + "service MUST deny access to the room and inform the user that they are unauthorized; this is done by returning " + + "a presence stanza of type \"error\" specifying a error.") + public void mucJoinPasswordProtectedRoomWrongPasswordTest() throws Exception { + EntityBareJid mucAddress = getRandomRoom("smack-inttest-enterpasswordprotectedroom"); + + MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); + + final Resourcepart nicknameOne = Resourcepart.from("one-" + randomString); + final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); + + final String correctPassword = StringUtils.insecureRandomString(8); + + createPasswordProtectedMuc(mucAsSeenByOne, nicknameOne, correctPassword); + + try { + // Second try: wrong password + XMPPException.XMPPErrorException wrongPasswordErrorException = assertThrows( + XMPPException.XMPPErrorException.class, + () -> mucAsSeenByTwo.join(nicknameTwo, correctPassword + "_"), + "Expected an error to be returned when '" + conTwo.getUser() + "' attempted to join password-protected room '" + mucAddress + "' using an incorrect password."); + assertNotNull(wrongPasswordErrorException.getStanzaError(), + "Expected an error to be returned when '" + conTwo.getUser() + "' attempted to join password-protected room '" + mucAddress + "' using an incorrect password."); + assertEquals(StanzaError.Condition.not_authorized, wrongPasswordErrorException.getStanzaError().getCondition(), + "Unexpected condition in the (expected) error that was returned when '" + conTwo.getUser() + "' attempted to join password-protected room '" + mucAddress + "' using an incorrect password."); + } finally { + mucAsSeenByOne.destroy(); + } + } + + /** + * Asserts that a user can enter a password-protected room when supplying the correct password. + * + * @throws Exception when errors occur + */ + @SmackIntegrationTest(section = "7.2.5", quote = + "If the room requires a password and the user did not supply one (or the password provided is incorrect), the " + + "service MUST deny access to the room and inform the user that they are unauthorized; this is done by returning " + + "a presence stanza of type \"error\" specifying a error.") + public void mucJoinPasswordProtectedRoomCorrectPasswordTest() throws Exception { + EntityBareJid mucAddress = getRandomRoom("smack-inttest-enterpasswordprotectedroom"); + + MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); + + final Resourcepart nicknameOne = Resourcepart.from("one-" + randomString); + final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); + + final String correctPassword = StringUtils.insecureRandomString(8); + + createPasswordProtectedMuc(mucAsSeenByOne, nicknameOne, correctPassword); + + // Set up to receive presence responses on successful join + final ResultSyncPoint participantTwoSyncPoint = new ResultSyncPoint<>(); + mucAsSeenByTwo.addParticipantListener(presence -> { + if (nicknameTwo.equals(presence.getFrom().getResourceOrEmpty())) { + participantTwoSyncPoint.signal(presence); + } + }); + + try { + // Third try: correct password + mucAsSeenByTwo.join(nicknameTwo, correctPassword); + Presence presenceCorrectPassword = assertResult(participantTwoSyncPoint, "Expected '" + conTwo.getUser() + "' to be able to join password-protected room '" + mucAddress + "' using the correct password (but no join-presence was received)."); + assertNull(presenceCorrectPassword.getError(), "Unexpected error in join-presence of '" + conTwo.getUser() + "' after joining password-protected room '" + mucAddress + "' using the correct password: " + presenceCorrectPassword.getError()); + } finally { + mucAsSeenByOne.destroy(); + } + } + + /** + * Asserts that a user can not enter a members-only room while not being a member. + * + * This test does not cover § 9.3, aka the happy path. + * + * @throws Exception when errors occur + */ + @SmackIntegrationTest(section = "7.2.6", quote = + "If the room is members-only but the user is not on the member list, the service MUST deny access to the " + + "room and inform the user that they are not allowed to enter the room; this is done by returning a presence " + + "stanza of type \"error\" specifying a error condition.") + public void mucJoinMembersOnlyRoomTest() throws Exception { + EntityBareJid mucAddress = getRandomRoom("smack-inttest-entermembersonlyroom"); + + MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); + + final Resourcepart nicknameOne = Resourcepart.from("one-" + randomString); + final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); + + createMembersOnlyMuc(mucAsSeenByOne, nicknameOne); + + try { + XMPPException.XMPPErrorException registrationRequiredErrorException = assertThrows( + XMPPException.XMPPErrorException.class, () -> mucAsSeenByTwo.join(nicknameTwo), + "Expected an error to be returned when non-member '" + conTwo.getUser() + "' tries to join member-online room '" + mucAddress + "' (but an error was not returned)."); + assertNotNull(registrationRequiredErrorException, "Expected an error to be returned when non-member '" + conTwo.getUser() + "' tries to join member-online room '" + mucAddress + "' (but an error was not returned)."); + assertNotNull(registrationRequiredErrorException.getStanzaError(), "Expected an error to be returned when non-member '" + conTwo.getUser() + "' tries to join member-online room '" + mucAddress + "' (but an error was not returned)."); + assertEquals(StanzaError.Condition.registration_required, registrationRequiredErrorException.getStanzaError().getCondition(), + "Unexpected condition in the (expected) error that was returned when non-member '" + conTwo.getUser() + "' joined member-online room '" + mucAddress + "' (."); + } finally { + mucAsSeenByOne.destroy(); + } + } + + /** + * Asserts that a user can not enter a room while being banned. + * + *

    From XEP-0045 § 7.2.7:

    + *
    + * + *
    + * + * @throws Exception when errors occur + */ + @SmackIntegrationTest(section = "7.2.7", quote = + "If the user has been banned from the room (i.e., has an affiliation of \"outcast\"), the service MUST deny " + + "access to the room and inform the user of the fact that they are banned; this is done by returning a presence " + + "stanza of type \"error\" specifying a error condition.") + public void mucBannedUserJoinRoomTest() throws Exception { + EntityBareJid mucAddress = getRandomRoom("smack-inttest-banneduser"); + + MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); + + final Resourcepart nicknameOne = Resourcepart.from("one-" + randomString); + final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); + + createMuc(mucAsSeenByOne, nicknameOne); + + mucAsSeenByOne.banUser(conTwo.getUser().asBareJid(), "Insufficient witchcraft"); + + try { + XMPPException.XMPPErrorException forbiddenErrorException = assertThrows( + XMPPException.XMPPErrorException.class, () -> mucAsSeenByTwo.join(nicknameTwo), + "Expected an error to be returned when outcast '" + conTwo.getUser() + "' tries to join room '" + mucAddress + "' (but an error was not returned)."); + assertNotNull(forbiddenErrorException, "Expected an error to be returned when outcast '" + conTwo.getUser() + "' tries to join room '" + mucAddress + "' (but an error was not returned)."); + assertNotNull(forbiddenErrorException.getStanzaError(), "Expected an error to be returned when outcast '" + conTwo.getUser() + "' tries to join room '" + mucAddress + "' (but an error was not returned)."); + assertEquals(StanzaError.Condition.forbidden, forbiddenErrorException.getStanzaError().getCondition(), + "Unexpected condition in the (expected) error to was returned when outcast '" + conTwo.getUser() + "' tried to join room '" + mucAddress + "'."); + } finally { + mucAsSeenByOne.destroy(); + } + } + + /** + * Asserts that a user can not enter a room with the same nickname as another user who is already present. + * + * @throws Exception when errors occur + */ + @SmackIntegrationTest(section = "7.2.8", quote = + "If the room already contains another user with the nickname desired by the user seeking to enter the room " + + "(or if the nickname is reserved by another user on the member list), the service MUST deny access to the " + + "room and inform the user of the conflict; this is done by returning a presence stanza of type \"error\" " + + "specifying a error condition.") + public void mucNicknameConflictJoinRoomTest() throws Exception { + EntityBareJid mucAddress = getRandomRoom("smack-inttest-nicknameclash"); + + MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); + + final Resourcepart nicknameOne = Resourcepart.from("one-" + randomString); + + createMuc(mucAsSeenByOne, nicknameOne); + + try { + XMPPException.XMPPErrorException conflictErrorException = assertThrows( + XMPPException.XMPPErrorException.class, () -> mucAsSeenByTwo.join(nicknameOne), + "Expected an error to be returned when '" + conTwo.getUser() + "' tried to join room '" + mucAddress + "' using the nickname '" + nicknameOne + "' that was already in used by another occupant of the room (but an error was not returned)."); + assertNotNull(conflictErrorException, "Expected an error to be returned when '" + conTwo.getUser() + "' tried to join room '" + mucAddress + "' using the nickname '" + nicknameOne + "' that was already in used by another occupant of the room (but an error was not returned)."); + assertNotNull(conflictErrorException.getStanzaError(), "Expected an error to be returned when '" + conTwo.getUser() + "' tried to join room '" + mucAddress + "' using the nickname '" + nicknameOne + "' that was already in used by another occupant of the room (but an error was not returned)."); + assertEquals(StanzaError.Condition.conflict, conflictErrorException.getStanzaError().getCondition(), + "Unexpected condition in the (expected) error that was returned when '" + conTwo.getUser() + "' tried to join room '" + mucAddress + "' using the nickname '" + nicknameOne + "' that was already in used by another occupant of the room."); + } finally { + mucAsSeenByOne.destroy(); + } + } + + /** + * Asserts that a room can not go past the configured maximum number of users, if a non-admin non-owner user + * attempts to join. + * + * @throws Exception when errors occur + */ + @SmackIntegrationTest(section = "7.2.9", quote = + "If the room has reached its maximum number of occupants, the service SHOULD deny access to the room and " + + "inform the user of the restriction; this is done by returning a presence stanza of type \"error\" " + + "specifying a error condition. Alternatively, the room could kick an \"idle user\" " + + "in order to free up space (where the definition of \"idle user\" is up to the implementation).") + public void mucMaxUsersLimitJoinRoomTest() throws Exception { + EntityBareJid mucAddress = getRandomRoom("smack-inttest-maxusersreached"); + + MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByThree = mucManagerThree.getMultiUserChat(mucAddress); + + final Resourcepart nicknameOne = Resourcepart.from("one-" + randomString); + final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); + final Resourcepart nicknameThree = Resourcepart.from("three-" + randomString); + + createMuc(mucAsSeenByOne, nicknameOne); + setMaxUsers(mucAsSeenByOne, 2); + + // Set up for participant 1 to receive presence responses on join of participant 2 + final ResultSyncPoint participantOneSeesTwoSyncPoint = new ResultSyncPoint<>(); + mucAsSeenByOne.addParticipantListener(presence -> { + if (nicknameTwo.equals(presence.getFrom().getResourceOrEmpty())) { + participantOneSeesTwoSyncPoint.signal(presence); + } + }); + + // Set up for participant 3 to receive presence responses on join of participant 3 + final ResultSyncPoint participantThreeSeesThreeSyncPoint = new ResultSyncPoint<>(); + mucAsSeenByThree.addParticipantListener(presence -> { + if (nicknameThree.equals(presence.getFrom().getResourceOrEmpty())) { + participantThreeSeesThreeSyncPoint.signal(presence); + } + }); + + try { + mucAsSeenByTwo.join(nicknameTwo); + participantOneSeesTwoSyncPoint.waitForResult(timeout); + + assertEquals(2, mucAsSeenByOne.getOccupantsCount(), "Unexpected occupant count as seen by '" + conOne.getUser() + "' in room '" + mucAddress + "' (prior to the join attempt of a third occupant)."); + + // Now user 3 may or may not be able to join the room. The service can either deny access to user three, or + // it can kick user 2. Both strategies would comply with the specification. So the only thing we can + // reasonably test here is whether the room doesn't have more occupants than its max size. + + XMPPException.XMPPErrorException errorException = assertThrows(XMPPException.XMPPErrorException.class, () -> mucAsSeenByThree.join(nicknameThree)); + + final StanzaError.Condition expectedCondition; + switch (sinttestConfiguration.compatibilityMode) { + default: + expectedCondition = StanzaError.Condition.service_unavailable; + break; + case ejabberd: + expectedCondition = StanzaError.Condition.resource_constraint; + break; + } + + StanzaError stanzaError = errorException.getStanzaError(); + assertNotNull(stanzaError); + + assertEquals(expectedCondition, stanzaError.getCondition()); + + // Now we should wait until participant one is informed about the (probably failed) new participant three + // room join. But if joining failed, there will be no such update. All we can reasonably do is wait until + // participant three has received its own presence response. This is not watertight though. + participantThreeSeesThreeSyncPoint.waitForResult(timeout); + + // Irrespective of the way the implementation handles max users, there should still be only 2 users. + assertEquals(2, mucAsSeenByOne.getOccupantsCount(), "Unexpected occupant count as seen by '" + conOne.getUser() + "' in room '" + mucAddress + "' (after the join attempt of a third occupant)."); + + } finally { + mucAsSeenByOne.destroy(); + } + } + + /** + * Asserts that an admin can still join a room that has reached the configured maximum number of users. + * + * @throws Exception when errors occur + */ + @SmackIntegrationTest(section = "7.2.9", quote = + "If the room has reached its maximum number of occupants and a room admin or owner attempts to join, the " + + "room MUST allow the admin or owner to join, up to some reasonable number of additional occupants; this " + + "helps to prevent denial of service attacks caused by stuffing the room with non-admin users.") + public void mucMaxUsersLimitAdminCanStillJoinRoomTest() throws Exception { + EntityBareJid mucAddress = getRandomRoom("smack-inttest-maxusersreached-adminjoin"); + + MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByThree = mucManagerThree.getMultiUserChat(mucAddress); + + final Resourcepart nicknameOne = Resourcepart.from("one-" + randomString); + final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); + final Resourcepart nicknameThree = Resourcepart.from("three-" + randomString); + + createMuc(mucAsSeenByOne, nicknameOne); + setMaxUsers(mucAsSeenByOne, 2); + + // Set up for participant 1 to receive presence responses on join of participant 2 + // Set up for participant 1 to receive presence responses on join of participant 3 + final ResultSyncPoint participantOneSeesTwoSyncPoint = new ResultSyncPoint<>(); + final ResultSyncPoint participantOneSeesThreeSyncPoint = new ResultSyncPoint<>(); + mucAsSeenByOne.addParticipantListener(presence -> { + if (nicknameTwo.equals(presence.getFrom().getResourceOrEmpty())) { + participantOneSeesTwoSyncPoint.signal(presence); + } else if (nicknameThree.equals(presence.getFrom().getResourceOrEmpty())) { + participantOneSeesThreeSyncPoint.signal(presence); + } + }); + + try { + mucAsSeenByTwo.join(nicknameTwo); + participantOneSeesTwoSyncPoint.waitForResult(timeout); + + assertEquals(2, mucAsSeenByOne.getOccupantsCount(), "Unexpected occupant count in room '" + mucAddress + "' (as observed by '" + conOne.getUser() + "') (prior to an admin attempting to join a room that was already having its maximum number of occupants)."); + + mucAsSeenByOne.grantAdmin(conThree.getUser().asBareJid()); // blocking call + mucAsSeenByThree.join(nicknameThree); + participantOneSeesThreeSyncPoint.waitForResult(timeout); + + assertNotNull(mucAsSeenByOne.getOccupant(JidCreate.entityFullFrom(mucAddress, nicknameThree)), "Expected admin '" + conThree.getUser() + "' to be in room '" + mucAddress + "' (as observed by '" + conOne.getUser() + "'), but was not."); + assertEquals(3, mucAsSeenByOne.getOccupantsCount(), "Unexpected occupant count in room '" + mucAddress + "' (as observed by '" + conOne.getUser() + "') (after an admin joined a room that was already having its maximum number of occupants)"); + } finally { + mucAsSeenByOne.destroy(); + } + } + + /** + * Asserts that an owner can still join a room that has reached the configured maximum number of users. + * + * @throws Exception when errors occur + */ + @SmackIntegrationTest(section = "7.2.9", quote = + "If the room has reached its maximum number of occupants and a room admin or owner attempts to join, the " + + "room MUST allow the admin or owner to join, up to some reasonable number of additional occupants; this " + + "helps to prevent denial of service attacks caused by stuffing the room with non-admin users.") + public void mucMaxUsersLimitOwnerCanStillJoinRoomTest() throws Exception { + EntityBareJid mucAddress = getRandomRoom("smack-inttest-maxusersreached-ownerjoin"); + + MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByThree = mucManagerThree.getMultiUserChat(mucAddress); + + final Resourcepart nicknameOne = Resourcepart.from("one-" + randomString); + final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); + final Resourcepart nicknameThree = Resourcepart.from("three-" + randomString); + + createMuc(mucAsSeenByOne, nicknameOne); + setMaxUsers(mucAsSeenByOne, 2); + + // Set up for participant 1 to receive presence responses on join of participant 2 + // Set up for participant 1 to receive presence responses on join of participant 3 + final ResultSyncPoint participantOneSeesTwoSyncPoint = new ResultSyncPoint<>(); + final ResultSyncPoint participantOneSeesThreeSyncPoint = new ResultSyncPoint<>(); + mucAsSeenByOne.addParticipantListener(presence -> { + if (nicknameTwo.equals(presence.getFrom().getResourceOrEmpty())) { + participantOneSeesTwoSyncPoint.signal(presence); + } else if (nicknameThree.equals(presence.getFrom().getResourceOrEmpty())) { + participantOneSeesThreeSyncPoint.signal(presence); + } + }); + + try { + mucAsSeenByTwo.join(nicknameTwo); + participantOneSeesTwoSyncPoint.waitForResult(timeout); + + assertEquals(2, mucAsSeenByOne.getOccupantsCount(), "Unexpected occupant count in room '" + mucAddress + "' (as observed by '" + conOne.getUser() + "') (prior to an owner attempting to join a room that was already having its maximum number of occupants)."); + + mucAsSeenByOne.grantOwnership(conThree.getUser().asBareJid()); // blocking call + mucAsSeenByThree.join(nicknameThree); + participantOneSeesThreeSyncPoint.waitForResult(timeout); + + assertNotNull(mucAsSeenByOne.getOccupant(JidCreate.entityFullFrom(mucAddress, nicknameThree)), "Expected owner '" + conThree.getUser() + "' to be in room '" + mucAddress + "' (as observed by '" + conOne.getUser() + "'), but was not."); + assertEquals(3, mucAsSeenByOne.getOccupantsCount(), "Unexpected occupant count in room '" + mucAddress + "' (as observed by '" + conOne.getUser() + "') (after an owner joined a room that was already having its maximum number of occupants)"); + + } finally { + mucAsSeenByOne.destroy(); + } + } + + /** + * Asserts that a room can not be entered while it still being created (locked). + * + * @throws Exception when errors occur + */ + @SmackIntegrationTest(section = "7.2.10", quote = + "If a user attempts to enter a room while it is \"locked\" (i.e., before the room creator provides an " + + "initial configuration and therefore before the room officially exists), the service MUST refuse entry and " + + "return an error to the user.") + public void mucJoinLockedRoomTest() throws Exception { + if (sinttestConfiguration.compatibilityMode == Configuration.CompatibilityMode.ejabberd) { + throw new TestNotPossibleException("ejabberd does not implement MUC locked rooms as per XEP-0045 § 7.2.10"); + } + + EntityBareJid mucAddress = getRandomRoom("smack-inttest-lockedroom"); + + MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); + + final Resourcepart nicknameOne = Resourcepart.from("one-" + randomString); + final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); + + // Note the absence of handle.makeInstant() here. The room is still being created at this point, until a + // configuration is set. + mucAsSeenByOne.create(nicknameOne); + + try { + XMPPException.XMPPErrorException conflictErrorException = assertThrows( + XMPPException.XMPPErrorException.class, () -> mucAsSeenByTwo.join(nicknameTwo), + "Expected an error to be returned when '" + conTwo.getUser() + "' tried to join room '" + mucAddress + "' that is still being created/is locked (but no error was returned)."); + assertNotNull(conflictErrorException, "Expected an error to be returned when '" + conTwo.getUser() + "' tried to join room '" + mucAddress + "' that is still being created/is locked (but no error was returned)."); + assertNotNull(conflictErrorException.getStanzaError(), "Expected an error to be returned when '" + conTwo.getUser() + "' tried to join room '" + mucAddress + "' that is still being created/is locked (but no error was returned)."); + assertEquals(StanzaError.Condition.item_not_found, conflictErrorException.getStanzaError().getCondition(), + "Unexpected condition in the (expected) error that was returned when '" + conTwo.getUser() + "' tried to join room '" + mucAddress + "' that is still being created/is locked."); + + } finally { + mucAsSeenByOne.destroy(); + } + } + + /** + * Asserts that a user is warned when entering a room that allows public logging. + * + * @throws Exception when errors occur + */ + @SmackIntegrationTest(section = "7.2.12", quote = + "If the user is entering a room in which the discussions are logged to a public archive (often accessible " + + "via HTTP), the service SHOULD allow the user to enter the room but MUST also warn the user that the " + + "discussions are logged. This is done by including a status code of \"170\" in the initial presence that the " + + "room sends to the new occupant.") + public void mucJoinRoomWithPublicLoggingTest() throws Exception { + final EntityBareJid mucAddress = getRandomRoom("smack-inttest-publiclogging"); + + final MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); + final MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); + + final Resourcepart nicknameOne = Resourcepart.from("one-" + randomString); + final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); + + createMuc(mucAsSeenByOne, nicknameOne); + + try { + mucAsSeenByOne.getConfigFormManager() + .enablePublicLogging() + .submitConfigurationForm(); + + Presence twoPresence = mucAsSeenByTwo.join(nicknameTwo); + assertTrue(MUCUser.from(twoPresence).getStatus().contains(MUCUser.Status.create(170)), + "Expected initial presence reflected to '" + conTwo.getUser() + "' when joining room '" + mucAddress + "' to include the status code '170' (but it did not)."); + } catch (MucConfigurationNotSupportedException e) { + throw new TestNotPossibleException(e); + } finally { + mucAsSeenByOne.destroy(); + } + } + + /** + * Asserts that all users in a room are correctly informed about nickname change of a participant. + * + * @throws Exception when errors occur + */ + @SmackIntegrationTest(section = "7.6", quote = + "A common feature of chat rooms is the ability for an occupant to change his or her nickname within the room. " + + "In MUC this is done by sending updated presence information to the room, specifically by sending presence to " + + "a new occupant JID in the same room (changing only the resource identifier in the occupant JID). The service " + + "then sends two presence stanzas to the full JID of each occupant (including the occupant who is changing his " + + "or her room nickname), one of type \"unavailable\" for the old nickname and one indicating availability for " + + "the new nickname. The unavailable presence MUST contain the following as extended presence information in an " + + "; element qualified by the 'http://jabber.org/protocol/muc#user' namespace: - The new nickname (in this " + + "case, nick='oldhag') - A status code of 303 This enables the recipients to correlate the old roomnick with " + + "the new roomnick.\n") + public void mucChangeNicknameInformationTest() throws Exception { + EntityBareJid mucAddress = getRandomRoom("smack-inttest-changenickname"); + + MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); + + final Resourcepart nicknameOne = Resourcepart.from("one-" + randomString); + final Resourcepart nicknameTwoOriginal = Resourcepart.from("two-original-" + randomString); + final Resourcepart nicknameTwoNew = Resourcepart.from("two-new-" + randomString); + + createMuc(mucAsSeenByOne, nicknameOne); + + try { + SimpleResultSyncPoint participantOneSeesTwoEnter = new SimpleResultSyncPoint(); + mucAsSeenByOne.addParticipantListener(presence -> { + if (nicknameTwoOriginal.equals(presence.getFrom().getResourceOrEmpty())) { + participantOneSeesTwoEnter.signal(); + } + }); + + // Have participant two enter the room + mucAsSeenByTwo.join(nicknameTwoOriginal); + participantOneSeesTwoEnter.waitForResult(timeout); + + // Although logic dictates that the 'unavailable' presence stanzas for the old nick should precede the presence + // stanza for the new nick - the specification does not dictate that. So we should allow for the order to be + // reversed. Here we will expect an unavailable and an available presence stanza sent to both participant one + // and participant two. So that adds up to a total of four. + MultiResultSyncPoint participantTwoPresencesSyncPoint = new MultiResultSyncPoint<>(4); + PresenceListener mucPresenceListener = presence -> { + Resourcepart fromResource = presence.getFrom().getResourceOrEmpty(); + if (nicknameTwoOriginal.equals(fromResource) || nicknameTwoNew.equals(fromResource)) { + participantTwoPresencesSyncPoint.signal(presence); + } + }; + mucAsSeenByOne.addParticipantListener(mucPresenceListener); + mucAsSeenByTwo.addParticipantListener(mucPresenceListener); + + // Participant two changes nickname + mucAsSeenByTwo.changeNickname(nicknameTwoNew); + final List partTwoPresencesReceived = participantTwoPresencesSyncPoint.waitForResults( + timeout); + + final List unavailablePresencesReceivedByOne = partTwoPresencesReceived.stream().filter( + presence -> !presence.isAvailable()).filter( + presence -> presence.getTo().equals(conOne.getUser().asEntityFullJidIfPossible())).collect( + Collectors.toList()); + final List unavailablePresencesReceivedByTwo = partTwoPresencesReceived.stream().filter( + presence -> !presence.isAvailable()).filter( + presence -> presence.getTo().equals(conTwo.getUser().asEntityFullJidIfPossible())).collect( + Collectors.toList()); + final List availablePresencesReceivedByOne = partTwoPresencesReceived.stream().filter( + Presence::isAvailable).filter( + presence -> presence.getTo().equals(conOne.getUser().asEntityFullJidIfPossible())).collect( + Collectors.toList()); + final List availablePresencesReceivedByTwo = partTwoPresencesReceived.stream().filter( + Presence::isAvailable).filter( + presence -> presence.getTo().equals(conTwo.getUser().asEntityFullJidIfPossible())).collect( + Collectors.toList()); + + // Validate that both users received both 'available' and 'unavailable' presence stanzas + assertEquals(1, unavailablePresencesReceivedByOne.size(), "Expected '" + conOne.getUser() + "' to have received one 'unavailable' presence when '" + conTwo.getUser() + "' changed its nickname in room '" + mucAddress + "' (but did not)"); + assertEquals(1, unavailablePresencesReceivedByTwo.size(), "Expected '" + conTwo.getUser() + "' to have received one 'unavailable' presence when they changed their nickname in room '" + mucAddress + "' (but did not)"); + assertEquals(1, availablePresencesReceivedByOne.size(), "Expected '" + conOne.getUser() + "' to have received one 'available' presence when '" + conTwo.getUser() + "' changed its nickname in room '" + mucAddress + "' (but did not)"); + assertEquals(1, availablePresencesReceivedByTwo.size(), "Expected '" + conTwo.getUser() + "' to have received one 'available' presence when they changed their nickname in room '" + mucAddress + "' (but did not)"); + + // Validate that the received 'unavailable' presence stanzas contain the status and items elements as specified + assertTrue(MUCUser.from(unavailablePresencesReceivedByOne.get(0)).getStatus().stream().anyMatch(status -> 303 == status.getCode()), + "Expected the 'unavailable' presence of user '" + conTwo.getUser() + "' reflecting their nickname change in room '" + mucAddress + "' as received by '" + conOne.getUser() + "' to include status code '303' (but it did not)."); + assertEquals(nicknameTwoNew, MUCUser.from(unavailablePresencesReceivedByOne.get(0)).getItem().getNick(), + "Expected the 'unavailable' presence of user '" + conTwo.getUser() + "' reflecting their nickname change in room '" + mucAddress + "' as received by '" + conOne.getUser() + "' to include the new nickname (but it did not)."); + assertTrue(MUCUser.from(unavailablePresencesReceivedByTwo.get(0)).getStatus().stream().anyMatch(status -> 303 == status.getCode()), + "Expected the 'unavailable' presence of user '" + conTwo.getUser() + "' reflecting their nickname change in room '" + mucAddress + "' as received by themselves to include status code '303' (but it did not)."); + assertEquals(nicknameTwoNew, MUCUser.from(unavailablePresencesReceivedByTwo.get(0)).getItem().getNick(), + "Expected the 'unavailable' presence of user '" + conTwo.getUser() + "' reflecting their nickname change in room '" + mucAddress + "' as received by themselves to include the new nickname (but it did not)."); + + // Validate that the received 'available' presence stanzas have the new nickname as from + assertEquals(nicknameTwoNew, availablePresencesReceivedByOne.get(0).getFrom().getResourceOrEmpty(), "Expected the 'available' presence of '" + conTwo.getUser() + "' as received by '" + conOne.getUser() + "' in room '" + mucAddress + "' to be sent 'from' their new nickname (but it was not)."); + assertEquals(nicknameTwoNew, availablePresencesReceivedByTwo.get(0).getFrom().getResourceOrEmpty(), "Expected the 'available' presence of '" + conTwo.getUser() + "' as received by themselves in room '" + mucAddress + "' to be sent 'from' their new nickname (but it was not)."); + + } finally { + tryDestroy(mucAsSeenByOne); + } + } + + /** + * Asserts that user can not change nickname to one that is already in use. + * + * @throws Exception when errors occur + */ + @SmackIntegrationTest(section = "7.6", quote = + "If the user attempts to change his or her room nickname to a room nickname that is already in use by " + + "another user (or that is reserved by another user affiliated with the room, e.g., a member or owner), the " + + "service MUST deny the nickname change request and inform the user of the conflict; this is done by " + + "returning a presence stanza of type \"error\" specifying a error condition:") + public void mucBlockChangeNicknameInformationTest() throws Exception { + EntityBareJid mucAddress = getRandomRoom("smack-inttest-blockchangenickname"); + + MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); + + final Resourcepart nicknameOne = Resourcepart.from("one-" + randomString); + final Resourcepart nicknameTwoOriginal = Resourcepart.from("two-original-" + randomString); + + createMuc(mucAsSeenByOne, nicknameOne); + + SimpleResultSyncPoint participantOneSeesTwoEnter = new SimpleResultSyncPoint(); + mucAsSeenByOne.addParticipantListener(presence -> { + if (nicknameTwoOriginal.equals(presence.getFrom().getResourceOrEmpty())) { + participantOneSeesTwoEnter.signal(); + } + }); + + // Have participant two enter the room + mucAsSeenByTwo.join(nicknameTwoOriginal); + participantOneSeesTwoEnter.waitForResult(timeout); + + try { + // Participant two changes nickname + XMPPException.XMPPErrorException conflictErrorException = assertThrows( + XMPPException.XMPPErrorException.class, () -> mucAsSeenByTwo.changeNickname(nicknameOne), + "Expected an error to be returned when '" + conTwo.getUser() + "' tried to change their nickname in room '" + mucAddress + "' to a nickname that was already in use by another occupant (but no error was returned)."); + assertNotNull(conflictErrorException, "Expected an error to be returned when '" + conTwo.getUser() + "' tried to change their nickname in room '" + mucAddress + "' to a nickname that was already in use by another occupant (but no error was returned)."); + assertNotNull(conflictErrorException.getStanzaError(), "Expected an error to be returned when '" + conTwo.getUser() + "' tried to change their nickname in room '" + mucAddress + "' to a nickname that was already in use by another occupant (but no error was returned)."); + assertEquals(StanzaError.Condition.conflict, conflictErrorException.getStanzaError().getCondition(), + "Unexpected conidtion in the (expected) error to was returned when '" + conTwo.getUser() + "' tried to change their nickname in room '" + mucAddress + "' to a nickname that was already in use by another occupant."); + } finally { + tryDestroy(mucAsSeenByOne); + } + } + + /** + * Asserts that when a user leaves a room, they are themselves included on the list of users notified (self-presence). + * + * @throws Exception when errors occur + */ + @SmackIntegrationTest(section = "7.14", quote = + "The service MUST then send a presence stanzas of type \"unavailable\" from the departing user's occupant " + + "JID to the departing occupant's full JIDs, including a status code of \"110\" to indicate that this " + + "notification is \"self-presence\"") + public void mucLeaveTest() throws Exception { + EntityBareJid mucAddress = getRandomRoom("smack-inttest-leave"); + + MultiUserChat muc = mucManagerOne.getMultiUserChat(mucAddress); + try { + muc.join(Resourcepart.from("nick-one")); + + Presence reflectedLeavePresence = muc.leave(); + + MUCUser mucUser = MUCUser.from(reflectedLeavePresence); + assertNotNull(mucUser, "Expected the reflected 'leave' presence of '" + conOne.getUser() + "' that left room '" + mucAddress + "' to include a valid 'user' child element (but it did not)."); + + assertTrue(mucUser.getStatus().contains(MUCUser.Status.PRESENCE_TO_SELF_110), "Expected the reflected 'leave' presence of '" + conOne.getUser() + "' that left room '" + mucAddress + "' to include status '" + MUCUser.Status.PRESENCE_TO_SELF_110 + "' (but it did not)."); + assertEquals(mucAddress + "/nick-one", reflectedLeavePresence.getFrom().toString(), "Unexpected 'from' attribute value in the reflected 'leave' presence of '" + conOne.getUser() + "' that left room '" + mucAddress + "'."); + assertEquals(conOne.getUser().asEntityFullJidIfPossible().toString(), reflectedLeavePresence.getTo().toString(), "Unexpected 'to' attribute value in the reflected 'leave' presence of '" + conOne.getUser() + "' that left room '" + mucAddress + "'."); + } finally { + muc.join(Resourcepart.from("nick-one")); // We need to be in the room to destroy the room + tryDestroy(muc); + } + } +} diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatRolesAffiliationsPrivilegesIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatRolesAffiliationsPrivilegesIntegrationTest.java index d26e22593..56c8a6dbb 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatRolesAffiliationsPrivilegesIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatRolesAffiliationsPrivilegesIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2021 Florian Schmaus, Dan Caseley + * Copyright 2021-2024 Florian Schmaus, Dan Caseley * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,61 +23,64 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.Set; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.packet.Presence; +import org.jivesoftware.smackx.muc.MultiUserChatException.MissingMucCreationAcknowledgeException; +import org.jivesoftware.smackx.muc.MultiUserChatException.MucAlreadyJoinedException; +import org.jivesoftware.smackx.muc.MultiUserChatException.NotAMucServiceException; import org.jivesoftware.smackx.muc.packet.MUCUser; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; import org.igniterealtime.smack.inttest.TestNotPossibleException; import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; +import org.igniterealtime.smack.inttest.annotations.SpecificationReference; import org.igniterealtime.smack.inttest.util.ResultSyncPoint; +import org.igniterealtime.smack.inttest.util.SimpleResultSyncPoint; import org.jxmpp.jid.EntityBareJid; import org.jxmpp.jid.EntityFullJid; import org.jxmpp.jid.Jid; import org.jxmpp.jid.impl.JidCreate; import org.jxmpp.jid.parts.Resourcepart; +import org.jxmpp.stringprep.XmppStringprepException; +@SpecificationReference(document = "XEP-0045", version = "1.34.6") public class MultiUserChatRolesAffiliationsPrivilegesIntegrationTest extends AbstractMultiUserChatIntegrationTest{ public MultiUserChatRolesAffiliationsPrivilegesIntegrationTest(SmackIntegrationTestEnvironment environment) throws SmackException.NoResponseException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, - InterruptedException, TestNotPossibleException { + InterruptedException, TestNotPossibleException, MucAlreadyJoinedException, MissingMucCreationAcknowledgeException, NotAMucServiceException, XmppStringprepException { super(environment); } /** - * Asserts that a user who undergoes a role change receives that change as a presence update - * - *

    From XEP-0045 § 5.1.3:

    - *
    - * ...a MUC service implementation MUST change the occupant's role to reflect the change and communicate the change - * to all occupants... - *
    - * - *

    From XEP-0045 § 9.6:

    - *
    - * The service MUST then send updated presence from this individual to all occupants, indicating the addition of - * moderator status... - *
    + * Asserts that a user who undergoes a role change receives that change as a presence update. * * @throws Exception when errors occur */ - @SmackIntegrationTest + @SmackIntegrationTest(section = "5.1.3", quote = + "(§ 5.1.3)... a MUC service implementation MUST change the occupant's role to reflect the change and " + + "communicate the change to all occupants [...] (§ 9.6) The service MUST then send updated presence from this " + + "individual to all occupants, indicating the addition of moderator status...") public void mucRoleTestForReceivingModerator() throws Exception { EntityBareJid mucAddress = getRandomRoom("smack-inttest"); MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); - final ResultSyncPoint resultSyncPoint = new ResultSyncPoint<>(); + final SimpleResultSyncPoint resultSyncPoint = new SimpleResultSyncPoint(); mucAsSeenByTwo.addUserStatusListener(new UserStatusListener() { @Override public void moderatorGranted() { - resultSyncPoint.signal("done"); + resultSyncPoint.signal(); } }); @@ -90,30 +93,21 @@ public class MultiUserChatRolesAffiliationsPrivilegesIntegrationTest extends Abs // success" in §9.6, since it'll throw on either an error IQ or on no response. mucAsSeenByOne.grantModerator(nicknameTwo); - resultSyncPoint.waitForResult(timeout); + assertResult(resultSyncPoint, "Expected " + conTwo.getUser() + " to get a presence update after it was granted the role 'moderator' role in " + mucAddress + " (but it did not)."); } finally { tryDestroy(mucAsSeenByOne); } } /** - * Asserts that a user who is present when another user undergoes a role change receives that change as a presence update - * - *

    From XEP-0045 § 5.1.3:

    - *
    - * ...a MUC service implementation MUST change the occupant's role to reflect the change and communicate the change - * to all occupants... - *
    - * - *

    From XEP-0045 § 9.6:

    - *
    - * The service MUST then send updated presence from this individual to all occupants, indicating the addition of - * moderator status... - *
    + * Asserts that a user who is present when another user undergoes a role change receives that change as a presence update. * * @throws Exception when errors occur */ - @SmackIntegrationTest + @SmackIntegrationTest(section = "9.6", quote = + "(§ 5.1.3)... a MUC service implementation MUST change the occupant's role to reflect the change and " + + "communicate the change to all occupants [...] (§ 9.6) The service MUST then send updated presence from this " + + "individual to all occupants, indicating the addition of moderator status...") public void mucRoleTestForWitnessingModerator() throws Exception { EntityBareJid mucAddress = getRandomRoom("smack-inttest"); @@ -121,12 +115,12 @@ public class MultiUserChatRolesAffiliationsPrivilegesIntegrationTest extends Abs MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); MultiUserChat mucAsSeenByThree = mucManagerThree.getMultiUserChat(mucAddress); - final ResultSyncPoint resultSyncPoint = new ResultSyncPoint<>(); + final SimpleResultSyncPoint resultSyncPoint = new SimpleResultSyncPoint(); mucAsSeenByThree.addParticipantStatusListener(new ParticipantStatusListener() { @Override public void moderatorGranted(EntityFullJid participant) { - resultSyncPoint.signal("done"); + resultSyncPoint.signal(); } }); @@ -138,7 +132,8 @@ public class MultiUserChatRolesAffiliationsPrivilegesIntegrationTest extends Abs mucAsSeenByThree.join(nicknameThree); mucAsSeenByOne.grantModerator(nicknameTwo); - resultSyncPoint.waitForResult(timeout); + + assertResult(resultSyncPoint, "Expected " + conThree.getUser() + " to get a presence update after another user in the room was granted the 'moderator' role in " + mucAddress + " (but it did not)."); } finally { tryDestroy(mucAsSeenByOne); } @@ -146,35 +141,26 @@ public class MultiUserChatRolesAffiliationsPrivilegesIntegrationTest extends Abs } /** - * Asserts that a user who undergoes a role change receives that change as a presence update - * - *

    From XEP-0045 § 5.1.3:

    - *
    - * ...a MUC service implementation MUST change the occupant's role to reflect the change and communicate the change - * to all occupants... - *
    - * - *

    From XEP-0045 § 9.7:

    - *
    - * The service MUST then send updated presence from this individual to all occupants, indicating the removal of - * moderator status... - *
    + * Asserts that a user who undergoes a role change receives that change as a presence update. * * @throws Exception when errors occur */ - @SmackIntegrationTest + @SmackIntegrationTest(section = "5.1.3", quote = + "(§ 5.1.3)... a MUC service implementation MUST change the occupant's role to reflect the change and " + + "communicate the change to all occupants [...] (§ 9.7) The service MUST then send updated presence from this " + + "individual to all occupants, indicating the removal of moderator status...") public void mucRoleTestForRemovingModerator() throws Exception { EntityBareJid mucAddress = getRandomRoom("smack-inttest"); MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); - final ResultSyncPoint resultSyncPoint = new ResultSyncPoint<>(); + final SimpleResultSyncPoint resultSyncPoint = new SimpleResultSyncPoint(); mucAsSeenByTwo.addUserStatusListener(new UserStatusListener() { @Override public void moderatorRevoked() { - resultSyncPoint.signal("done"); + resultSyncPoint.signal(); } }); @@ -185,30 +171,22 @@ public class MultiUserChatRolesAffiliationsPrivilegesIntegrationTest extends Abs mucAsSeenByOne.grantModerator(nicknameTwo); mucAsSeenByOne.revokeModerator(nicknameTwo); - resultSyncPoint.waitForResult(timeout); + + assertResult(resultSyncPoint, "Expected " + conTwo.getUser() + " to get a presence update after its 'moderator' role in " + mucAddress + " was revoked (but it did not)."); } finally { tryDestroy(mucAsSeenByOne); } } /** - * Asserts that a user who is present when another user undergoes a role change receives that change as a presence update - * - *

    From XEP-0045 § 5.1.3:

    - *
    - * ...a MUC service implementation MUST change the occupant's role to reflect the change and communicate the change - * to all occupants... - *
    - * - *

    From XEP-0045 § 9.7:

    - *
    - * The service MUST then send updated presence from this individual to all occupants, indicating the removal of - * moderator status... - *
    + * Asserts that a user who is present when another user undergoes a role change receives that change as a presence update. * * @throws Exception when errors occur */ - @SmackIntegrationTest + @SmackIntegrationTest(section = "9.7", quote = + "(§ 5.1.3)... a MUC service implementation MUST change the occupant's role to reflect the change and " + + "communicate the change to all occupants [...] (§ 9.7) The service MUST then send updated presence from this " + + "individual to all occupants, indicating the removal of moderator status...") public void mucRoleTestForWitnessingModeratorRemoval() throws Exception { EntityBareJid mucAddress = getRandomRoom("smack-inttest"); @@ -216,12 +194,12 @@ public class MultiUserChatRolesAffiliationsPrivilegesIntegrationTest extends Abs MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); MultiUserChat mucAsSeenByThree = mucManagerThree.getMultiUserChat(mucAddress); - final ResultSyncPoint resultSyncPoint = new ResultSyncPoint<>(); + final SimpleResultSyncPoint resultSyncPoint = new SimpleResultSyncPoint(); mucAsSeenByThree.addParticipantStatusListener(new ParticipantStatusListener() { @Override public void moderatorRevoked(EntityFullJid participant) { - resultSyncPoint.signal("done"); + resultSyncPoint.signal(); } }); @@ -234,42 +212,33 @@ public class MultiUserChatRolesAffiliationsPrivilegesIntegrationTest extends Abs mucAsSeenByOne.grantModerator(nicknameTwo); mucAsSeenByOne.revokeModerator(nicknameTwo); - resultSyncPoint.waitForResult(timeout); + assertResult(resultSyncPoint, "Expected " + conThree.getUser() + " to get a presence update after the 'moderator' role of another user in the room was revoked in " + mucAddress + " (but it did not)."); } finally { tryDestroy(mucAsSeenByOne); } } /** - * Asserts that a user in an unmoderated room who undergoes an afilliation change receives that change as a presence update - * - *

    From XEP-0045 § 5.1.3:

    - *
    - * ...a MUC service implementation MUST change the occupant's role to reflect the change and communicate the change - * to all occupants... - *
    - * - *

    From XEP-0045 § 8.4:

    - *
    - * The service MUST then send updated presence from this individual to all occupants, indicating the removal of - * voice privileges... - *
    + * Asserts that a user in an unmoderated room who undergoes an affiliation change receives that change as a presence update. * * @throws Exception when errors occur */ - @SmackIntegrationTest + @SmackIntegrationTest(section = "5.1.3", quote = + "(§ 5.1.3)... a MUC service implementation MUST change the occupant's role to reflect the change and " + + "communicate the change to all occupants [...] (§ 8.4) The service MUST then send updated presence from " + + "this individual to all occupants, indicating the removal of voice privileges...") public void mucRoleTestForRevokingVoice() throws Exception { EntityBareJid mucAddress = getRandomRoom("smack-inttest"); MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); - final ResultSyncPoint resultSyncPoint = new ResultSyncPoint<>(); + final SimpleResultSyncPoint resultSyncPoint = new SimpleResultSyncPoint(); mucAsSeenByTwo.addUserStatusListener(new UserStatusListener() { @Override public void voiceRevoked() { - resultSyncPoint.signal("done"); + resultSyncPoint.signal(); } }); @@ -278,30 +247,21 @@ public class MultiUserChatRolesAffiliationsPrivilegesIntegrationTest extends Abs final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); mucAsSeenByTwo.join(nicknameTwo); mucAsSeenByOne.revokeVoice(nicknameTwo); - resultSyncPoint.waitForResult(timeout); + assertResult(resultSyncPoint, "Expected " + conTwo.getUser() + " to get a presence update after its 'voice' privilege was revoked in " + mucAddress + " (but it did not)."); } finally { tryDestroy(mucAsSeenByOne); } } /** - * Asserts that a user who is present when another user undergoes a role change receives that change as a presence update - * - *

    From XEP-0045 § 5.1.3:

    - *
    - * ...a MUC service implementation MUST change the occupant's role to reflect the change and communicate the change - * to all occupants... - *
    - * - *

    From XEP-0045 § 8.4:

    - *
    - * The service MUST then send updated presence from this individual to all occupants, indicating the removal of - * voice privileges... - *
    + * Asserts that a user who is present when another user undergoes a role change receives that change as a presence update. * * @throws Exception when errors occur */ - @SmackIntegrationTest + @SmackIntegrationTest(section = "8.4", quote = + "(§ 5.1.3)... a MUC service implementation MUST change the occupant's role to reflect the change and " + + "communicate the change to all occupants [...] (§ 8.4) The service MUST then send updated presence from " + + "this individual to all occupants, indicating the removal of voice privileges...") public void mucRoleTestForWitnessingRevokingVoice() throws Exception { EntityBareJid mucAddress = getRandomRoom("smack-inttest"); @@ -309,12 +269,12 @@ public class MultiUserChatRolesAffiliationsPrivilegesIntegrationTest extends Abs MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); MultiUserChat mucAsSeenByThree = mucManagerThree.getMultiUserChat(mucAddress); - final ResultSyncPoint resultSyncPoint = new ResultSyncPoint<>(); + final SimpleResultSyncPoint resultSyncPoint = new SimpleResultSyncPoint(); mucAsSeenByThree.addParticipantStatusListener(new ParticipantStatusListener() { @Override public void voiceRevoked(EntityFullJid participant) { - resultSyncPoint.signal("done"); + resultSyncPoint.signal(); } }); @@ -326,43 +286,34 @@ public class MultiUserChatRolesAffiliationsPrivilegesIntegrationTest extends Abs mucAsSeenByThree.join(nicknameThree); mucAsSeenByOne.revokeVoice(nicknameTwo); - resultSyncPoint.waitForResult(timeout); + assertResult(resultSyncPoint, "Expected " + conThree.getUser() + " to get a presence update after another user's 'voice' privilege was revoked in " + mucAddress + " (but it did not)."); } finally { tryDestroy(mucAsSeenByOne); } } /** - * Asserts that a user who undergoes an affiliation change receives that change as a presence update - * - *

    From XEP-0045 § 5.2.2:

    - *
    - * ...a MUC service implementation MUST change the user's affiliation to reflect the change and communicate that - * to all occupants... - *
    - * - *

    From XEP-0045 § 10.6:

    - *
    - * If the user is in the room, the service MUST then send updated presence from this individual to all occupants, - * indicating the granting of admin status... - *
    + * Asserts that a user who undergoes an affiliation change receives that change as a presence update. * * @throws Exception when errors occur */ - @SmackIntegrationTest + @SmackIntegrationTest(section = "5.2.2", quote = + "(§ 5.2.2) ... a MUC service implementation MUST change the user's affiliation to reflect the change and " + + "communicate that to all occupants [...] (§ 10.6) If the user is in the room, the service MUST then send " + + "updated presence from this individual to all occupants, indicating the granting of admin status...") public void mucAffiliationTestForReceivingAdmin() throws Exception { EntityBareJid mucAddress = getRandomRoom("smack-inttest"); MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); - final ResultSyncPoint resultSyncPoint = new ResultSyncPoint<>(); + final SimpleResultSyncPoint resultSyncPoint = new SimpleResultSyncPoint(); mucAsSeenByTwo.addUserStatusListener(new UserStatusListener() { @Override public void adminGranted() { - resultSyncPoint.signal("done"); + resultSyncPoint.signal(); } }); @@ -373,7 +324,7 @@ public class MultiUserChatRolesAffiliationsPrivilegesIntegrationTest extends Abs // This implicitly tests "The service MUST add the user to the admin list and then inform the owner of success" in §10.6, since it'll throw on either an error IQ or on no response. mucAsSeenByOne.grantAdmin(conTwo.getUser().asBareJid()); - resultSyncPoint.waitForResult(timeout); + assertResult(resultSyncPoint, "Expected " + conTwo.getUser() + " to get a presence update after its was granted 'admin' affiliation in " + mucAddress + " (but it did not)."); } finally { tryDestroy(mucAsSeenByOne); } @@ -381,23 +332,14 @@ public class MultiUserChatRolesAffiliationsPrivilegesIntegrationTest extends Abs /** * Asserts that a user who is present when another user undergoes an affiliation change receives that change as a - * presence update - * - *

    From XEP-0045 § 5.2.2:

    - *
    - * ...a MUC service implementation MUST change the user's affiliation to reflect the change and communicate that - * to all occupants... - *
    - * - *

    From XEP-0045 § 10.6:

    - *
    - * If the user is in the room, the service MUST then send updated presence from this individual to all occupants, - * indicating the granting of admin status... - *
    + * presence update. * * @throws Exception when errors occur */ - @SmackIntegrationTest + @SmackIntegrationTest(section = "10.6", quote = + "(§ 5.2.2) ... a MUC service implementation MUST change the user's affiliation to reflect the change and " + + "communicate that to all occupants [...] (§ 10.6) If the user is in the room, the service MUST then send " + + "updated presence from this individual to all occupants, indicating the granting of admin status...") public void mucAffiliationTestForWitnessingAdmin() throws Exception { EntityBareJid mucAddress = getRandomRoom("smack-inttest"); @@ -405,12 +347,12 @@ public class MultiUserChatRolesAffiliationsPrivilegesIntegrationTest extends Abs MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); MultiUserChat mucAsSeenByThree = mucManagerThree.getMultiUserChat(mucAddress); - final ResultSyncPoint resultSyncPoint = new ResultSyncPoint<>(); + final SimpleResultSyncPoint resultSyncPoint = new SimpleResultSyncPoint(); mucAsSeenByThree.addParticipantStatusListener(new ParticipantStatusListener() { @Override public void adminGranted(EntityFullJid participant) { - resultSyncPoint.signal("done"); + resultSyncPoint.signal(); } }); @@ -422,42 +364,34 @@ public class MultiUserChatRolesAffiliationsPrivilegesIntegrationTest extends Abs mucAsSeenByThree.join(nicknameThree); mucAsSeenByOne.grantAdmin(conTwo.getUser().asBareJid()); - resultSyncPoint.waitForResult(timeout); + assertResult(resultSyncPoint, "Expected " + conThree.getUser() + " to get a presence update after another user was granted 'admin' affiliation in " + mucAddress + " (but it did not)."); } finally { tryDestroy(mucAsSeenByOne); } } /** - * Asserts that a user who undergoes an affiliation change receives that change as a presence update - * - *

    From XEP-0045 § 5.2.2:

    - *
    - * ...a MUC service implementation MUST change the user's affiliation to reflect the change and communicate that to - * all occupants... - *
    - * - *

    From XEP-0045 § 10.7:

    - *
    - * If the user is in the room, the service MUST then send updated presence from this individual to all occupants, - * indicating the loss of admin status by sending a presence element... - *
    + * Asserts that a user who undergoes an affiliation change receives that change as a presence update. * * @throws Exception when errors occur */ - @SmackIntegrationTest + @SmackIntegrationTest(section = "10.7", quote = + "(§ 5.2.2) ... a MUC service implementation MUST change the user's affiliation to reflect the change and " + + "communicate that to all occupants [...] (§ 10.6) If the user is in the room, the service MUST then send " + + "updated presence from this individual to all occupants, indicating the loss of admin status by sending a " + + "presence element...") public void mucAffiliationTestForRemovingAdmin() throws Exception { EntityBareJid mucAddress = getRandomRoom("smack-inttest"); MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); - final ResultSyncPoint resultSyncPoint = new ResultSyncPoint<>(); + final SimpleResultSyncPoint resultSyncPoint = new SimpleResultSyncPoint(); mucAsSeenByTwo.addUserStatusListener(new UserStatusListener() { @Override public void adminRevoked() { - resultSyncPoint.signal("done"); + resultSyncPoint.signal(); } }); @@ -468,7 +402,7 @@ public class MultiUserChatRolesAffiliationsPrivilegesIntegrationTest extends Abs mucAsSeenByOne.grantAdmin(conTwo.getUser().asBareJid()); mucAsSeenByOne.revokeAdmin(conTwo.getUser().asEntityBareJid()); - resultSyncPoint.waitForResult(timeout); + assertResult(resultSyncPoint, "Expected " + conTwo.getUser() + " to get a presence update after its 'admin' affiliation was revoked in " + mucAddress + " (but it did not)."); } finally { tryDestroy(mucAsSeenByOne); } @@ -491,7 +425,11 @@ public class MultiUserChatRolesAffiliationsPrivilegesIntegrationTest extends Abs * * @throws Exception when errors occur */ - @SmackIntegrationTest + @SmackIntegrationTest(section = "10.7", quote = + "(§ 5.2.2) ... a MUC service implementation MUST change the user's affiliation to reflect the change and " + + "communicate that to all occupants [...] (§ 10.6) If the user is in the room, the service MUST then send " + + "updated presence from this individual to all occupants, indicating the loss of admin status by sending a " + + "presence element...") public void mucAffiliationTestForWitnessingAdminRemoval() throws Exception { EntityBareJid mucAddress = getRandomRoom("smack-inttest"); @@ -499,12 +437,12 @@ public class MultiUserChatRolesAffiliationsPrivilegesIntegrationTest extends Abs MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); MultiUserChat mucAsSeenByThree = mucManagerThree.getMultiUserChat(mucAddress); - final ResultSyncPoint resultSyncPoint = new ResultSyncPoint<>(); + final SimpleResultSyncPoint resultSyncPoint = new SimpleResultSyncPoint(); mucAsSeenByThree.addParticipantStatusListener(new ParticipantStatusListener() { @Override public void adminRevoked(EntityFullJid participant) { - resultSyncPoint.signal("done"); + resultSyncPoint.signal(); } }); @@ -517,28 +455,23 @@ public class MultiUserChatRolesAffiliationsPrivilegesIntegrationTest extends Abs mucAsSeenByOne.grantAdmin(conTwo.getUser().asBareJid()); mucAsSeenByOne.revokeAdmin(conTwo.getUser().asEntityBareJid()); - resultSyncPoint.waitForResult(timeout); + assertResult(resultSyncPoint, "Expected " + conThree.getUser() + " to get a presence update after another user's 'admin' affiliation was revoked in " + mucAddress + " (but it did not)."); } finally { tryDestroy(mucAsSeenByOne); } } /** - * Asserts that a user who gets kicked receives that change as a presence update - * - *

    From XEP-0045 § 8.2:

    - *
    - * The kick is performed based on the occupant's room nickname and is completed by setting the role of a - * participant or visitor to a value of "none". - * - * The service MUST remove the kicked occupant by sending a presence stanza of type "unavailable" to each kicked - * occupant, including status code 307 in the extended presence information, optionally along with the reason (if - * provided) and the roomnick or bare JID of the user who initiated the kick. - *
    + * Asserts that a user who gets kicked receives that change as a presence update. * * @throws Exception when errors occur */ - @SmackIntegrationTest + @SmackIntegrationTest(section = "8.2", quote = + "The kick is performed based on the occupant's room nickname and is completed by setting the role of a " + + "participant or visitor to a value of \"none\". The service MUST remove the kicked occupant by sending a " + + "presence stanza of type \"unavailable\" to each kicked occupant, including status code 307 in the extended " + + "presence information, optionally along with the reason (if provided) and the roomnick or bare JID of the " + + "user who initiated the kick.") public void mucPresenceTestForGettingKicked() throws Exception { EntityBareJid mucAddress = getRandomRoom("smack-inttest"); @@ -556,35 +489,33 @@ public class MultiUserChatRolesAffiliationsPrivilegesIntegrationTest extends Abs mucAsSeenByOne.kickParticipant(nicknameTwo, "Nothing personal. Just a test."); Presence kickPresence = resultSyncPoint.waitForResult(timeout); MUCUser mucUser = MUCUser.from(kickPresence); - assertNotNull(mucUser); + assertNotNull(mucUser, "Expected, but unable, to create a MUCUser instance from 'kick' presence: " + kickPresence); assertAll( - () -> assertTrue(mucUser.getStatus().contains(MUCUser.Status.PRESENCE_TO_SELF_110), "Missing self-presence status code in kick presence"), - () -> assertTrue(mucUser.getStatus().contains(MUCUser.Status.KICKED_307), "Missing kick status code in kick presence"), - () -> assertEquals(MUCRole.none, mucUser.getItem().getRole(), "Role other than 'none' in kick presence") + () -> assertTrue(mucUser.getStatus().contains(MUCUser.Status.PRESENCE_TO_SELF_110), "Missing self-presence status code in kick presence received by " + conTwo.getUser() + " after being kicked from room " + mucAddress), + () -> assertTrue(mucUser.getStatus().contains(MUCUser.Status.KICKED_307), "Missing kick status code in kick presence received by " + conTwo.getUser() + " after being kicked from room " + mucAddress), + () -> assertEquals(MUCRole.none, mucUser.getItem().getRole(), "Role other than 'none' in kick presence received by " + conTwo.getUser() + " after being kicked from room " + mucAddress) ); Jid itemJid = mucUser.getItem().getJid(); if (itemJid != null) { - assertEquals(conTwo.getUser().asEntityFullJidIfPossible(), itemJid, "Incorrect kicked user in kick presence"); + assertEquals(conTwo.getUser().asEntityFullJidIfPossible(), itemJid, "Incorrect kicked user in kick presence received by " + conTwo.getUser() + " after being kicked from room " + mucAddress); } + } catch (TimeoutException e) { + fail("Expected " + conTwo.getUser() + " to receive a presence update after it was kicked from room " + mucAddress + " (but it did not).", e); } finally { tryDestroy(mucAsSeenByOne); } } /** - * Asserts that a user who is present when another user gets kicked receives that change as a presence update - * - *

    From XEP-0045 § 8.2:

    - *
    - * ...the service MUST then inform all of the remaining occupants that the kicked occupant is no longer in the room - * by sending presence stanzas of type "unavailable" from the individual's roomnick (<room@service/nick>) to all - * the remaining occupants (just as it does when occupants exit the room of their own volition), including the - * status code and optionally the reason and actor. - *
    + * Asserts that a user who is present when another user gets kicked receives that change as a presence update. * * @throws Exception when errors occur */ - @SmackIntegrationTest + @SmackIntegrationTest(section = "8.2", quote = + "...the service MUST then inform all of the remaining occupants that the kicked occupant is no longer in the " + + "room by sending presence stanzas of type \"unavailable\" from the individual's roomnick " + + "() to all the remaining occupants (just as it does when occupants exit the room of their " + + "own volition), including the status code and optionally the reason and actor.") public void mucPresenceTestForWitnessingKick() throws Exception { EntityBareJid mucAddress = getRandomRoom("smack-inttest"); @@ -605,16 +536,18 @@ public class MultiUserChatRolesAffiliationsPrivilegesIntegrationTest extends Abs mucAsSeenByOne.kickParticipant(nicknameTwo, "Nothing personal. Just a test."); Presence kickPresence = resultSyncPoint.waitForResult(timeout); MUCUser mucUser = MUCUser.from(kickPresence); - assertNotNull(mucUser); + assertNotNull(mucUser, "Expected, but unable, to create a MUCUser instance from 'kick' presence: " + kickPresence); assertAll( - () -> assertFalse(mucUser.getStatus().contains(MUCUser.Status.PRESENCE_TO_SELF_110), "Incorrect self-presence status code in kick presence"), - () -> assertTrue(mucUser.getStatus().contains(MUCUser.Status.KICKED_307), "Missing kick status code in kick presence"), - () -> assertEquals(MUCRole.none, mucUser.getItem().getRole(), "Role other than 'none' in kick presence") + () -> assertFalse(mucUser.getStatus().contains(MUCUser.Status.PRESENCE_TO_SELF_110), "Incorrect self-presence status code in kick presence received by " + conThree.getUser() + " after another user was kicked from room " + mucAddress), + () -> assertTrue(mucUser.getStatus().contains(MUCUser.Status.KICKED_307), "Missing kick status code in kick presence received by " + conThree.getUser() + " after another user was kicked from room " + mucAddress), + () -> assertEquals(MUCRole.none, mucUser.getItem().getRole(), "Role other than 'none' in kick presence received by " + conThree.getUser() + " after another user was kicked from room " + mucAddress) ); Jid itemJid = mucUser.getItem().getJid(); if (itemJid != null) { - assertEquals(conTwo.getUser().asEntityFullJidIfPossible(), itemJid, "Incorrect kicked user in kick presence"); + assertEquals(conTwo.getUser().asEntityFullJidIfPossible(), itemJid, "Incorrect kicked user in kick presence received by " + conThree.getUser() + " after another user was kicked from room " + mucAddress); } + } catch (TimeoutException e) { + fail("Expected " + conThree.getUser() + " to receive a presence update after another user was kicked from room " + mucAddress + " (but it did not).", e); } finally { tryDestroy(mucAsSeenByOne); } @@ -624,16 +557,12 @@ public class MultiUserChatRolesAffiliationsPrivilegesIntegrationTest extends Abs /** * Asserts that an affiliation is persistent between visits to the room. * - *

    From XEP-0045 § 5.2:

    - *
    - * These affiliations are long-lived in that they persist across a user's visits to the room and are not affected - * by happenings in the room...Affiliations are granted, revoked, and maintained based on the user's bare JID, not - * the nick as with roles. - *
    - * * @throws Exception when errors occur */ - @SmackIntegrationTest + @SmackIntegrationTest(section = "5.2", quote = + "These affiliations are long-lived in that they persist across a user's visits to the room and are not " + + "affected by happenings in the room...Affiliations are granted, revoked, and maintained based on the user's " + + "bare JID, not the nick as with roles.") public void mucTestPersistentAffiliation() throws Exception { EntityBareJid mucAddress = getRandomRoom("smack-inttest"); @@ -657,32 +586,24 @@ public class MultiUserChatRolesAffiliationsPrivilegesIntegrationTest extends Abs mucAsSeenByThree.leave(); Presence p2 = mucAsSeenByTwo.join(nicknameTwo); Presence p3 = mucAsSeenByThree.join(nicknameThree); - assertEquals(MUCAffiliation.owner, MUCUser.from(p2).getItem().getAffiliation()); - assertEquals(MUCAffiliation.admin, MUCUser.from(p3).getItem().getAffiliation()); + assertEquals(MUCAffiliation.owner, MUCUser.from(p2).getItem().getAffiliation(), "Unexpected affiliation of " + conTwo.getUser() + " after it re-joined room " + mucAddress); + assertEquals(MUCAffiliation.admin, MUCUser.from(p3).getItem().getAffiliation(), "Unexpected affiliation of " + conThree.getUser() + " after it re-joined room " + mucAddress); } finally { tryDestroy(mucAsSeenByOne); } } /** - * Asserts that a moderator cannot revoke voice from an owner - * - *

    From XEP-0045 § 5.1.1:

    - *
    - * A moderator MUST NOT be able to revoke voice privileges from an admin or owner - *
    - * - *

    From XEP-0045 § 8.4:

    - *
    - * A moderator MUST NOT be able to revoke voice from a user whose affiliation is at or above the moderator's level. - * In addition, a service MUST NOT allow the voice privileges of an admin or owner to be removed by anyone. If a - * moderator attempts to revoke voice privileges from such a user, the service MUST deny the request and return a - * <not-allowed/> error to the sender along with the offending item(s) - *
    + * Asserts that a moderator cannot revoke voice from an owner. * * @throws Exception when errors occur */ - @SmackIntegrationTest + @SmackIntegrationTest(section = "5.1.1", quote = + "A moderator MUST NOT be able to revoke voice privileges from an admin or owner [...] (§ 8.4) A moderator " + + "MUST NOT be able to revoke voice from a user whose affiliation is at or above the moderator's level. In " + + "addition, a service MUST NOT allow the voice privileges of an admin or owner to be removed by anyone. If a " + + "moderator attempts to revoke voice privileges from such a user, the service MUST deny the request and return " + + "a error to the sender along with the offending item(s)") public void mucTestModeratorCannotRevokeVoiceFromOwner() throws Exception { EntityBareJid mucAddress = getRandomRoom("smack-inttest"); @@ -697,8 +618,9 @@ public class MultiUserChatRolesAffiliationsPrivilegesIntegrationTest extends Abs mucAsSeenByTwo.join(nicknameTwo); mucAsSeenByOne.grantModerator(nicknameTwo); XMPPException.XMPPErrorException xe = assertThrows(XMPPException.XMPPErrorException.class, - () -> mucAsSeenByTwo.revokeVoice(nicknameOne)); - assertEquals(xe.getStanzaError().getCondition().toString(), "not-allowed"); + () -> mucAsSeenByTwo.revokeVoice(nicknameOne), + "Expected an XMPP error when " + conTwo.getUser() + " was trying to revoke the 'voice' privilege of " + conOne.getUser() + " in room " + mucAddress); + assertEquals("not-allowed", xe.getStanzaError().getCondition().toString(), "Unexpected stanza error condition in error returned when " + conTwo.getUser() + " was trying to revoke the 'voice' privilege of " + conOne.getUser() + " in room " + mucAddress); } finally { tryDestroy(mucAsSeenByOne); } @@ -708,16 +630,12 @@ public class MultiUserChatRolesAffiliationsPrivilegesIntegrationTest extends Abs * Asserts that a moderator cannot revoke moderator privileges from a moderator with a higher affiliation * than themselves. * - *

    From XEP-0045 § 5.1.3 and §5.2.1:

    - *
    - * A moderator SHOULD NOT be allowed to revoke moderation privileges from someone with a higher affiliation than - * themselves (i.e., an unaffiliated moderator SHOULD NOT be allowed to revoke moderation privileges from an admin - * or an owner, and an admin SHOULD NOT be allowed to revoke moderation privileges from an owner) - *
    - * * @throws Exception when errors occur */ - @SmackIntegrationTest + @SmackIntegrationTest(section = "5.1.3", quote = + "A moderator SHOULD NOT be allowed to revoke moderation privileges from someone with a higher affiliation " + + "than themselves (i.e., an unaffiliated moderator SHOULD NOT be allowed to revoke moderation privileges from " + + "an admin or an owner, and an admin SHOULD NOT be allowed to revoke moderation privileges from an owner)") public void mucTestModeratorCannotBeRevokedFromHigherAffiliation() throws Exception { EntityBareJid mucAddress = getRandomRoom("smack-inttest"); @@ -739,32 +657,31 @@ public class MultiUserChatRolesAffiliationsPrivilegesIntegrationTest extends Abs // Admin cannot revoke from Owner XMPPException.XMPPErrorException xe1 = assertThrows(XMPPException.XMPPErrorException.class, - () -> mucAsSeenByTwo.revokeModerator(nicknameOne)); - // Moderator cannot revoke from Admin - XMPPException.XMPPErrorException xe2 = assertThrows(XMPPException.XMPPErrorException.class, - () -> mucAsSeenByThree.revokeModerator(nicknameOne)); + () -> mucAsSeenByTwo.revokeModerator(nicknameOne), + "Expected an XMPP error when " + conTwo.getUser() + " (an admin) was trying to revoke the 'moderator' role of " + conOne.getUser() + " (an owner) in room " + mucAddress); // Moderator cannot revoke from Owner + XMPPException.XMPPErrorException xe2 = assertThrows(XMPPException.XMPPErrorException.class, + () -> mucAsSeenByThree.revokeModerator(nicknameOne), + "Expected an XMPP error when " + conThree.getUser() + " (a moderator) was trying to revoke the 'moderator' role of " + conOne.getUser() + " (an owner) in room " + mucAddress); + // Moderator cannot revoke from Admin XMPPException.XMPPErrorException xe3 = assertThrows(XMPPException.XMPPErrorException.class, - () -> mucAsSeenByThree.revokeModerator(nicknameTwo)); - assertEquals(xe1.getStanzaError().getCondition().toString(), "not-allowed"); - assertEquals(xe2.getStanzaError().getCondition().toString(), "not-allowed"); - assertEquals(xe3.getStanzaError().getCondition().toString(), "not-allowed"); + () -> mucAsSeenByThree.revokeModerator(nicknameTwo), + "Expected an XMPP error when " + conThree.getUser() + " (a moderator) was trying to revoke the 'moderator' role of " + conTwo.getUser() + " (an admin) in room " + mucAddress); + assertEquals("not-allowed", xe1.getStanzaError().getCondition().toString(), "Unexpected condition in XMPP error when " + conTwo.getUser() + " (an admin) was trying to revoke the 'moderator' role of " + conOne.getUser() + " (an owner) in room " + mucAddress); + assertEquals("not-allowed", xe2.getStanzaError().getCondition().toString(), "Unexpected condition in XMPP error when " + conThree.getUser() + " (a moderator) was trying to revoke the 'moderator' role of " + conOne.getUser() + " (an owner) in room " + mucAddress); + assertEquals("not-allowed", xe3.getStanzaError().getCondition().toString(), "Unexpected condition in XMPP error when " + conThree.getUser() + " (a moderator) was trying to revoke the 'moderator' role of " + conTwo.getUser() + " (an admin) in room " + mucAddress); } finally { tryDestroy(mucAsSeenByOne); } } /** - * Asserts that an unmoderated room assigns the correct default roles for a given affiliation - * - *

    From XEP-0045 § 5.1.2:

    - *
    - * ...the initial default roles that a service SHOULD set based on the user's affiliation... - *
    + * Asserts that an unmoderated room assigns the correct default roles for a given affiliation. * * @throws Exception when errors occur */ - @SmackIntegrationTest + @SmackIntegrationTest(section = "5.1.2", quote = + "...the initial default roles that a service SHOULD set based on the user's affiliation...") public void mucTestDefaultRoleForAffiliationInUnmoderatedRoom() throws Exception { EntityBareJid mucAddress = getRandomRoom("smack-inttest-unmoderatedroles"); @@ -776,44 +693,48 @@ public class MultiUserChatRolesAffiliationsPrivilegesIntegrationTest extends Abs final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); final Resourcepart nicknameThree = Resourcepart.from("three-" + randomString); + final EntityFullJid jidOne = JidCreate.entityFullFrom(mucAddress, nicknameOne); + final EntityFullJid jidTwo = JidCreate.entityFullFrom(mucAddress, nicknameTwo); + final EntityFullJid jidThree = JidCreate.entityFullFrom(mucAddress, nicknameThree); + createMuc(mucAsSeenByOne, nicknameOne); + + final SimpleResultSyncPoint allOccupantsDetectedSyncPoint = new SimpleResultSyncPoint(); + final Set expectedOccupants = Set.of(jidOne, jidTwo, jidThree); + mucAsSeenByOne.addParticipantStatusListener(new ParticipantStatusListener() { + @Override + public void joined(EntityFullJid participant) { + if (mucAsSeenByOne.getOccupants().containsAll(expectedOccupants)) { + allOccupantsDetectedSyncPoint.signal(); + } + } + }); + try { + mucAsSeenByOne.grantAdmin(conTwo.getUser().asBareJid()); + mucAsSeenByTwo.join(nicknameTwo); mucAsSeenByThree.join(nicknameThree); - final ResultSyncPoint resultSyncPoint = new ResultSyncPoint<>(); - mucAsSeenByOne.addParticipantStatusListener(new ParticipantStatusListener() { - @Override - public void adminGranted(EntityFullJid participant) { - resultSyncPoint.signal("done"); - } - }); - mucAsSeenByOne.grantAdmin(conTwo.getUser().asBareJid()); - resultSyncPoint.waitForResult(timeout); - - assertEquals(mucAsSeenByOne.getOccupantsCount(), 3); - assertEquals(MUCRole.moderator, mucAsSeenByOne.getOccupant( - JidCreate.entityFullFrom(mucAddress, nicknameOne)).getRole()); - assertEquals(MUCRole.moderator, mucAsSeenByOne.getOccupant( - JidCreate.entityFullFrom(mucAddress, nicknameTwo)).getRole()); - assertEquals(MUCRole.participant, mucAsSeenByOne.getOccupant( - JidCreate.entityFullFrom(mucAddress, nicknameThree)).getRole()); + assertResult(allOccupantsDetectedSyncPoint, "Expected " + conOne.getUser() + " to observe all of these occupants in room " + mucAddress + ", but not all of them appear to be in: " + expectedOccupants.stream().map(Object::toString).collect(Collectors.joining(", "))); + assertEquals(MUCRole.moderator, mucAsSeenByOne.getOccupant(JidCreate.entityFullFrom(mucAddress, nicknameOne)).getRole(), + "Unexpected role for occupant " + nicknameOne + " of " + mucAddress); + assertEquals(MUCRole.moderator, mucAsSeenByOne.getOccupant(JidCreate.entityFullFrom(mucAddress, nicknameTwo)).getRole(), + "Unexpected role for occupant " + nicknameTwo + " of " + mucAddress); + assertEquals(MUCRole.participant, mucAsSeenByOne.getOccupant(JidCreate.entityFullFrom(mucAddress, nicknameThree)).getRole(), + "Unexpected role for occupant " + nicknameThree + " of " + mucAddress); } finally { tryDestroy(mucAsSeenByOne); } } /** - * Asserts that a moderated room assigns the correct default roles for a given affiliation - * - *

    From XEP-0045 § 5.1.2:

    - *
    - * ...the initial default roles that a service SHOULD set based on the user's affiliation... - *
    + * Asserts that a moderated room assigns the correct default roles for a given affiliation. * * @throws Exception when errors occur */ - @SmackIntegrationTest + @SmackIntegrationTest(section = "5.1.2", quote = + "...the initial default roles that a service SHOULD set based on the user's affiliation...") public void mucTestDefaultRoleForAffiliationInModeratedRoom() throws Exception { EntityBareJid mucAddress = getRandomRoom("smack-inttest-moderatedroles"); @@ -825,16 +746,23 @@ public class MultiUserChatRolesAffiliationsPrivilegesIntegrationTest extends Abs final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); final Resourcepart nicknameThree = Resourcepart.from("three-" + randomString); - final ResultSyncPoint resultSyncPoint = new ResultSyncPoint<>(); - mucAsSeenByOne.addParticipantStatusListener(new ParticipantStatusListener() { - @Override - public void adminGranted(EntityFullJid participant) { - resultSyncPoint.signal("done"); - } - }); + final EntityFullJid jidOne = JidCreate.entityFullFrom(mucAddress, nicknameOne); + final EntityFullJid jidTwo = JidCreate.entityFullFrom(mucAddress, nicknameTwo); + final EntityFullJid jidThree = JidCreate.entityFullFrom(mucAddress, nicknameThree); createModeratedMuc(mucAsSeenByOne, nicknameOne); + final SimpleResultSyncPoint allOccupantsDetectedSyncPoint = new SimpleResultSyncPoint(); + final Set expectedOccupants = Set.of(jidOne, jidTwo, jidThree); + mucAsSeenByOne.addParticipantStatusListener(new ParticipantStatusListener() { + @Override + public void joined(EntityFullJid participant) { + if (mucAsSeenByOne.getOccupants().containsAll(expectedOccupants)) { + allOccupantsDetectedSyncPoint.signal(); + } + } + }); + final MUCRole threeRole; switch (sinttestConfiguration.compatibilityMode) { default: @@ -846,34 +774,30 @@ public class MultiUserChatRolesAffiliationsPrivilegesIntegrationTest extends Abs } try { + mucAsSeenByOne.grantAdmin(conTwo.getUser().asBareJid()); + mucAsSeenByTwo.join(nicknameTwo); mucAsSeenByThree.join(nicknameThree); - mucAsSeenByOne.grantAdmin(conTwo.getUser().asBareJid()); - resultSyncPoint.waitForResult(timeout); - assertEquals(mucAsSeenByOne.getOccupantsCount(), 3); - assertEquals(MUCRole.moderator, mucAsSeenByOne.getOccupant( - JidCreate.entityFullFrom(mucAddress, nicknameOne)).getRole()); - assertEquals(MUCRole.moderator, mucAsSeenByOne.getOccupant( - JidCreate.entityFullFrom(mucAddress, nicknameTwo)).getRole()); - assertEquals(threeRole, mucAsSeenByOne.getOccupant( - JidCreate.entityFullFrom(mucAddress, nicknameThree)).getRole()); + assertResult(allOccupantsDetectedSyncPoint, "Expected " + conOne.getUser() + " to observe all of these occupants in room " + mucAddress + ", but not all of them appear to be in: " + expectedOccupants.stream().map(Object::toString).collect(Collectors.joining(", "))); + assertEquals(MUCRole.moderator, mucAsSeenByOne.getOccupant(JidCreate.entityFullFrom(mucAddress, nicknameOne)).getRole(), + "Unexpected role for occupant " + nicknameOne + " of " + mucAddress); + assertEquals(MUCRole.moderator, mucAsSeenByOne.getOccupant(JidCreate.entityFullFrom(mucAddress, nicknameTwo)).getRole(), + "Unexpected role for occupant " + nicknameTwo + " of " + mucAddress); + assertEquals(threeRole, mucAsSeenByOne.getOccupant(JidCreate.entityFullFrom(mucAddress, nicknameThree)).getRole(), + "Unexpected role for occupant " + nicknameThree + " of " + mucAddress); } finally { tryDestroy(mucAsSeenByOne); } } /** - * Asserts that a members-only room assigns the correct default roles for a given affiliation - * - *

    From XEP-0045 § 5.1.2:

    - *
    - * ...the initial default roles that a service SHOULD set based on the user's affiliation... - *
    + * Asserts that a members-only room assigns the correct default roles for a given affiliation. * * @throws Exception when errors occur */ - @SmackIntegrationTest + @SmackIntegrationTest(section = "5.1.2", quote = + "...the initial default roles that a service SHOULD set based on the user's affiliation...") public void mucTestDefaultRoleForAffiliationInMembersOnlyRoom() throws Exception { EntityBareJid mucAddress = getRandomRoom("smack-inttest-membersonlyroles"); @@ -891,26 +815,29 @@ public class MultiUserChatRolesAffiliationsPrivilegesIntegrationTest extends Abs createMembersOnlyMuc(mucAsSeenByOne, nicknameOne); - final ResultSyncPoint adminResultSyncPoint = new ResultSyncPoint<>(); + final SimpleResultSyncPoint allOccupantsDetectedSyncPoint = new SimpleResultSyncPoint(); + final Set expectedOccupants = Set.of(jidOne, jidTwo, jidThree); mucAsSeenByOne.addParticipantStatusListener(new ParticipantStatusListener() { @Override - public void adminGranted(EntityFullJid participant) { - adminResultSyncPoint.signal("done"); + public void joined(EntityFullJid participant) { + if (mucAsSeenByOne.getOccupants().containsAll(expectedOccupants)) { + allOccupantsDetectedSyncPoint.signal(); + } } }); try { mucAsSeenByOne.grantMembership(conTwo.getUser().asBareJid()); mucAsSeenByOne.grantMembership(conThree.getUser().asBareJid()); + mucAsSeenByOne.grantAdmin(conTwo.getUser().asBareJid()); mucAsSeenByTwo.join(nicknameTwo); mucAsSeenByThree.join(nicknameThree); - mucAsSeenByOne.grantAdmin(conTwo.getUser().asBareJid()); - adminResultSyncPoint.waitForResult(timeout); - assertEquals(mucAsSeenByOne.getOccupantsCount(), 3); - assertEquals(MUCRole.moderator, mucAsSeenByOne.getOccupant(jidOne).getRole()); - assertEquals(MUCRole.moderator, mucAsSeenByOne.getOccupant(jidTwo).getRole()); - assertEquals(MUCRole.participant, mucAsSeenByOne.getOccupant(jidThree).getRole()); + + assertResult(allOccupantsDetectedSyncPoint, "Expected " + conOne.getUser() + " to observe all of these occupants in room " + mucAddress + ", but not all of them appear to be in: " + expectedOccupants.stream().map(Object::toString).collect(Collectors.joining(", "))); + assertEquals(MUCRole.moderator, mucAsSeenByOne.getOccupant(jidOne).getRole(), "Unexpected role for occupant " + jidOne + " in room " + mucAddress); + assertEquals(MUCRole.moderator, mucAsSeenByOne.getOccupant(jidTwo).getRole(), "Unexpected role for occupant " + jidTwo + " in room " + mucAddress); + assertEquals(MUCRole.participant, mucAsSeenByOne.getOccupant(jidThree).getRole(), "Unexpected role for occupant " + jidThree + " in room " + mucAddress); } finally { tryDestroy(mucAsSeenByOne); } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/ParticipantStatusIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/ParticipantStatusIntegrationTest.java new file mode 100644 index 000000000..1bf3fdce4 --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/ParticipantStatusIntegrationTest.java @@ -0,0 +1,133 @@ +/** + * + * Copyright 2024 Guus der Kinderen + * + * 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.SmackException; +import org.jivesoftware.smack.XMPPException; + +import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; +import org.igniterealtime.smack.inttest.TestNotPossibleException; +import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; +import org.igniterealtime.smack.inttest.annotations.SpecificationReference; +import org.igniterealtime.smack.inttest.util.SimpleResultSyncPoint; +import org.jxmpp.jid.EntityBareJid; +import org.jxmpp.jid.EntityFullJid; +import org.jxmpp.jid.impl.JidCreate; +import org.jxmpp.jid.parts.Resourcepart; +import org.jxmpp.stringprep.XmppStringprepException; + +/** + * Tests that verify the correct functionality of Smack's {@link ParticipantStatusListener}. + */ +@SpecificationReference(document = "XEP-0045", version = "1.34.6") +public class ParticipantStatusIntegrationTest extends AbstractMultiUserChatIntegrationTest { + + public ParticipantStatusIntegrationTest(SmackIntegrationTestEnvironment environment) throws SmackException.NoResponseException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, TestNotPossibleException, MultiUserChatException.MucAlreadyJoinedException, MultiUserChatException.MissingMucCreationAcknowledgeException, XmppStringprepException, MultiUserChatException.NotAMucServiceException { + super(environment); + } + + /** + * Verifies that when a member gets its membership removed in an open room, the appropriate event listener is invoked. + * + * @throws Exception On unexpected results + */ + @SmackIntegrationTest(section = "9.4", quote = "An admin might want to revoke a user's membership [...] The service MUST then send updated presence from this individual to all occupants, indicating the loss of membership by sending a presence element that contains an element qualified by the 'http://jabber.org/protocol/muc#user' namespace and containing an child with the 'affiliation' attribute set to a value of \"none\".") + public void testMembershipRevokedInOpenRoom() throws Exception { + // Setup test fixture. + final EntityBareJid mucAddress = getRandomRoom("smack-inttest-participantstatus-membership-revoked-open"); + final MultiUserChat mucAsSeenByOwner = mucManagerOne.getMultiUserChat(mucAddress); + final MultiUserChat mucAsSeenByTarget = mucManagerTwo.getMultiUserChat(mucAddress); + + final EntityFullJid mucAddressOwner = JidCreate.entityFullFrom(mucAddress, Resourcepart.from("owner-" + randomString)); + final EntityFullJid mucAddressTarget = JidCreate.entityFullFrom(mucAddress, Resourcepart.from("target-" + randomString)); + + createMuc(mucAsSeenByOwner, mucAddressOwner.getResourcepart()); + try { + mucAsSeenByOwner.grantMembership(conTwo.getUser().asBareJid()); + mucAsSeenByTarget.join(mucAddressTarget.getResourcepart()); + + final SimpleResultSyncPoint ownerSeesRevoke = new SimpleResultSyncPoint(); + mucAsSeenByOwner.addParticipantStatusListener(new ParticipantStatusListener() { + @Override + public void membershipRevoked(EntityFullJid participant) { + if (mucAddressTarget.equals(participant)) { + ownerSeesRevoke.signal(); + } + } + }); + + // Execute system under test. + mucAsSeenByOwner.revokeMembership(conTwo.getUser().asBareJid()); + + // Verify result. + assertResult(ownerSeesRevoke, "Expected '" + conOne.getUser() + "' to be notified of the revocation of membership of '" + conTwo.getUser() + "' (using nickname '" + mucAddressTarget.getResourcepart() + "') in '" + mucAddress + "' (but did not)."); + } finally { + // Clean up test fixture. + tryDestroy(mucAsSeenByOwner); + } + } + + /** + * Verifies that when a member gets its membership removed in a members-only room, the appropriate event listeners are invoked. + * + * @throws Exception On unexpected results + */ + @SmackIntegrationTest(section = "9.4", quote = "An admin might want to revoke a user's membership [...] If the room is members-only, the service MUST remove the user from the room, including a status code of 321 to indicate that the user was removed because of an affiliation change, and inform all remaining occupants") + public void testMembershipRevokedInMemberOnlyRoom() throws Exception { + // Setup test fixture. + final EntityBareJid mucAddress = getRandomRoom("smack-inttest-participantstatus-membership-revoked-membersonly"); + final MultiUserChat mucAsSeenByOwner = mucManagerOne.getMultiUserChat(mucAddress); + final MultiUserChat mucAsSeenByTarget = mucManagerTwo.getMultiUserChat(mucAddress); + + final EntityFullJid mucAddressOwner = JidCreate.entityFullFrom(mucAddress, Resourcepart.from("owner-" + randomString)); + final EntityFullJid mucAddressTarget = JidCreate.entityFullFrom(mucAddress, Resourcepart.from("target-" + randomString)); + + createMembersOnlyMuc(mucAsSeenByOwner, mucAddressOwner.getResourcepart()); + try { + mucAsSeenByOwner.grantMembership(conTwo.getUser().asBareJid()); + mucAsSeenByTarget.join(mucAddressTarget.getResourcepart()); + + final SimpleResultSyncPoint ownerSeesRevoke = new SimpleResultSyncPoint(); + final SimpleResultSyncPoint ownerSeesDeparture = new SimpleResultSyncPoint(); + mucAsSeenByOwner.addParticipantStatusListener(new ParticipantStatusListener() { + @Override + public void membershipRevoked(EntityFullJid participant) { + if (mucAddressTarget.equals(participant)) { + ownerSeesRevoke.signal(); + } + } + + @Override + public void parted(EntityFullJid participant) { + if (mucAddressTarget.equals(participant)) { + ownerSeesDeparture.signal(); + } + } + }); + + // Execute system under test. + mucAsSeenByOwner.revokeMembership(conTwo.getUser().asBareJid()); + + // Verify result. + assertResult(ownerSeesRevoke, "Expected '" + conOne.getUser() + "' to be notified of the revocation of membership of '" + conTwo.getUser() + "' (using nickname '" + mucAddressTarget.getResourcepart() + "') in '" + mucAddress + "' (but did not)."); + assertResult(ownerSeesDeparture, "Expected '" + conOne.getUser() + "' to be notified of '" + conTwo.getUser() + "' (using nickname '" + mucAddressTarget.getResourcepart() + "') departing '" + mucAddress + "' (but did not)."); + } finally { + // Clean up test fixture. + tryDestroy(mucAsSeenByOwner); + } + } +} diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/UserStatusIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/UserStatusIntegrationTest.java new file mode 100644 index 000000000..201f1ba21 --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/UserStatusIntegrationTest.java @@ -0,0 +1,129 @@ +/** + * + * Copyright 2024 Guus der Kinderen + * + * 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.SmackException; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.packet.Presence; +import org.jivesoftware.smackx.muc.packet.MUCUser; + +import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; +import org.igniterealtime.smack.inttest.TestNotPossibleException; +import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; +import org.igniterealtime.smack.inttest.annotations.SpecificationReference; +import org.igniterealtime.smack.inttest.util.SimpleResultSyncPoint; +import org.jxmpp.jid.EntityBareJid; +import org.jxmpp.jid.EntityFullJid; +import org.jxmpp.jid.impl.JidCreate; +import org.jxmpp.jid.parts.Resourcepart; +import org.jxmpp.stringprep.XmppStringprepException; + +/** + * Tests that verify the correct functionality of Smack's {@link UserStatusListener}. + */ +@SpecificationReference(document = "XEP-0045", version = "1.34.6") +public class UserStatusIntegrationTest extends AbstractMultiUserChatIntegrationTest { + + public UserStatusIntegrationTest(SmackIntegrationTestEnvironment environment) throws SmackException.NoResponseException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, TestNotPossibleException, MultiUserChatException.MucAlreadyJoinedException, MultiUserChatException.MissingMucCreationAcknowledgeException, XmppStringprepException, MultiUserChatException.NotAMucServiceException { + super(environment); + } + + /** + * Verifies that when a member gets its membership removed in an open room, the appropriate event listener is invoked. + * + * @throws Exception On unexpected results + */ + @SmackIntegrationTest(section = "9.4", quote = "An admin might want to revoke a user's membership [...] The service MUST then send updated presence from this individual to all occupants, indicating the loss of membership by sending a presence element that contains an element qualified by the 'http://jabber.org/protocol/muc#user' namespace and containing an child with the 'affiliation' attribute set to a value of \"none\".") + public void testMembershipRevokedInOpenRoom() throws Exception { + // Setup test fixture. + final EntityBareJid mucAddress = getRandomRoom("smack-inttest-userstatus-membership-revoked-membersonly"); + final MultiUserChat mucAsSeenByOwner = mucManagerOne.getMultiUserChat(mucAddress); + final MultiUserChat mucAsSeenByTarget = mucManagerTwo.getMultiUserChat(mucAddress); + + final EntityFullJid mucAddressOwner = JidCreate.entityFullFrom(mucAddress, Resourcepart.from("owner-" + randomString)); + final EntityFullJid mucAddressTarget = JidCreate.entityFullFrom(mucAddress, Resourcepart.from("target-" + randomString)); + + createMuc(mucAsSeenByOwner, mucAddressOwner.getResourcepart()); + try { + mucAsSeenByOwner.grantMembership(conTwo.getUser().asBareJid()); + mucAsSeenByTarget.join(mucAddressTarget.getResourcepart()); + + final SimpleResultSyncPoint targetSeesRevoke = new SimpleResultSyncPoint(); + mucAsSeenByTarget.addUserStatusListener(new UserStatusListener() { + @Override + public void membershipRevoked() { + targetSeesRevoke.signal(); + } + }); + + // Execute system under test. + mucAsSeenByOwner.revokeMembership(conTwo.getUser().asBareJid()); + + // Verify result. + assertResult(targetSeesRevoke, "Expected '" + conTwo.getUser() + "' (using nickname '" + mucAddressTarget.getResourcepart() + "') to be notified that their membership status was removed by '" + conOne.getUser() + "' (using nickname '" + mucAddressOwner.getResourcepart() + "') in '" + mucAddress + "' (but did not)."); + } finally { + // Clean up test fixture. + tryDestroy(mucAsSeenByOwner); + } + } + + /** + * Verifies that when a member gets its membership removed in a members-only room, the appropriate event listeners are invoked. + * + * @throws Exception On unexpected results + */ + @SmackIntegrationTest(section = "9.4", quote = "An admin might want to revoke a user's membership [...] If the room is members-only, the service MUST remove the user from the room, including a status code of 321 to indicate that the user was removed because of an affiliation change, and inform all remaining occupants") + public void testMembershipRevokedInMemberOnlyRoom() throws Exception { + // Setup test fixture. + final EntityBareJid mucAddress = getRandomRoom("smack-inttest-userstatus-membership-revoked-membersonly"); + final MultiUserChat mucAsSeenByOwner = mucManagerOne.getMultiUserChat(mucAddress); + final MultiUserChat mucAsSeenByTarget = mucManagerTwo.getMultiUserChat(mucAddress); + + final EntityFullJid mucAddressOwner = JidCreate.entityFullFrom(mucAddress, Resourcepart.from("owner-" + randomString)); + final EntityFullJid mucAddressTarget = JidCreate.entityFullFrom(mucAddress, Resourcepart.from("target-" + randomString)); + + createMembersOnlyMuc(mucAsSeenByOwner, mucAddressOwner.getResourcepart()); + try { + mucAsSeenByOwner.grantMembership(conTwo.getUser().asBareJid()); + mucAsSeenByTarget.join(mucAddressTarget.getResourcepart()); + + final SimpleResultSyncPoint targetSeesRevoke = new SimpleResultSyncPoint(); + final SimpleResultSyncPoint targetSeesRemove = new SimpleResultSyncPoint(); + mucAsSeenByTarget.addUserStatusListener(new UserStatusListener() { + @Override + public void removed(MUCUser mucUser, Presence presence) { + targetSeesRemove.signal(); + } + + @Override + public void membershipRevoked() { + targetSeesRevoke.signal(); + } + }); + + // Execute system under test. + mucAsSeenByOwner.revokeMembership(conTwo.getUser().asBareJid()); + + // Verify result. + assertResult(targetSeesRemove, "Expected '" + conTwo.getUser() + "' (using nickname '" + mucAddressTarget.getResourcepart() + "') to be notified that it is removed from '" + mucAddress + "' which is a member-only room, as their membership status was removed by '" + conOne.getUser() + "' (using nickname '" + mucAddressOwner.getResourcepart() + "') (but did not)."); + assertResult(targetSeesRevoke, "Expected '" + conTwo.getUser() + "' (using nickname '" + mucAddressTarget.getResourcepart() + "') to be notified that their membership status was removed by '" + conOne.getUser() + "' (using nickname '" + mucAddressOwner.getResourcepart() + "') in '" + mucAddress + "' (but did not)."); + } finally { + // Clean up test fixture. + tryDestroy(mucAsSeenByOwner); + } + } +} diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractTwoUsersOmemoIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractTwoUsersOmemoIntegrationTest.java index 1720c0725..995fc14a8 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractTwoUsersOmemoIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractTwoUsersOmemoIntegrationTest.java @@ -17,7 +17,7 @@ package org.jivesoftware.smackx.omemo; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import java.io.IOException; @@ -51,7 +51,9 @@ public abstract class AbstractTwoUsersOmemoIntegrationTest extends AbstractOmemo alice = OmemoManagerSetupHelper.prepareOmemoManager(conOne); bob = OmemoManagerSetupHelper.prepareOmemoManager(conTwo); - assertFalse(alice.getDeviceId().equals(bob.getDeviceId())); + // TODO is this a test assertion, or a bug in the test implementation (in which case an Exception should be thrown instead). + assertNotEquals(alice.getDeviceId(), bob.getDeviceId(), + "Expected device ID for " + conOne.getUser() + " to differ from that of " + conTwo.getUser() + " (but they did not)"); // Subscribe presences IntegrationTestRosterUtil.ensureBothAccountsAreSubscribedToEachOther(alice.getConnection(), bob.getConnection(), timeout); @@ -59,8 +61,10 @@ public abstract class AbstractTwoUsersOmemoIntegrationTest extends AbstractOmemo OmemoManagerSetupHelper.trustAllIdentitiesWithTests(alice, bob); // Alice trusts Bob's devices OmemoManagerSetupHelper.trustAllIdentitiesWithTests(bob, alice); // Bob trusts Alice' and Mallory's devices - assertEquals(bob.getOwnFingerprint(), alice.getActiveFingerprints(bob.getOwnJid()).get(bob.getOwnDevice())); - assertEquals(alice.getOwnFingerprint(), bob.getActiveFingerprints(alice.getOwnJid()).get(alice.getOwnDevice())); + assertEquals(bob.getOwnFingerprint(), alice.getActiveFingerprints(bob.getOwnJid()).get(bob.getOwnDevice()), + "Expected fingerprint of " + conTwo.getUser() + "'s device as known to " + conOne.getUser() + " to be equal to " + conTwo.getUser() + "'s own fingerprint (but it was not)."); + assertEquals(alice.getOwnFingerprint(), bob.getActiveFingerprints(alice.getOwnJid()).get(alice.getOwnDevice()), + "Expected fingerprint of " + conOne.getUser() + "'s device as known to " + conTwo.getUser() + " to be equal to " + conOne.getUser() + "'s own fingerprint (but it was not)."); } @AfterClass diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/MessageEncryptionIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/MessageEncryptionIntegrationTest.java index 2efd1fb1a..5b489aef7 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/MessageEncryptionIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/MessageEncryptionIntegrationTest.java @@ -29,12 +29,14 @@ import org.jivesoftware.smackx.omemo.element.OmemoBundleElement; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; import org.igniterealtime.smack.inttest.TestNotPossibleException; import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; +import org.igniterealtime.smack.inttest.annotations.SpecificationReference; /** * Simple OMEMO message encryption integration test. * During this test Alice sends an encrypted message to Bob. Bob decrypts it and sends a response to Alice. * It is checked whether the messages can be decrypted, and if used up pre-keys result in renewed bundles. */ +@SpecificationReference(document = "XEP-0384", version = "0.3.0") public class MessageEncryptionIntegrationTest extends AbstractTwoUsersOmemoIntegrationTest { public MessageEncryptionIntegrationTest(SmackIntegrationTestEnvironment environment) diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoMamDecryptionTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoMamDecryptionTest.java index 442643071..f14fd1990 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoMamDecryptionTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoMamDecryptionTest.java @@ -35,11 +35,13 @@ import org.jivesoftware.smackx.omemo.util.MessageOrOmemoMessage; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; import org.igniterealtime.smack.inttest.TestNotPossibleException; import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; +import org.igniterealtime.smack.inttest.annotations.SpecificationReference; /** * This test sends a message from Alice to Bob, while Bob has automatic decryption disabled. * Then Bob fetches his Mam archive and decrypts the result. */ +@SpecificationReference(document = "XEP-0384", version = "0.3.0") public class OmemoMamDecryptionTest extends AbstractTwoUsersOmemoIntegrationTest { public OmemoMamDecryptionTest(SmackIntegrationTestEnvironment environment) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, @@ -71,11 +73,12 @@ public class OmemoMamDecryptionTest extends AbstractTwoUsersOmemoIntegrationTest alicesConnection.sendStanza(encrypted.buildMessage(messageBuilder, bob.getOwnJid())); MamManager.MamQuery query = bobsMamManager.queryArchive(MamManager.MamQueryArgs.builder().limitResultsToJid(alice.getOwnJid()).build()); - assertEquals(1, query.getMessageCount()); + assertEquals(1, query.getMessageCount(), "Unexpected message count in MAM query result of " + bob.getConnection().getUser()); List decryptedMamQuery = bob.decryptMamQueryResult(query); - assertEquals(1, decryptedMamQuery.size()); - assertEquals(body, decryptedMamQuery.get(decryptedMamQuery.size() - 1).getOmemoMessage().getBody()); + assertEquals(1, decryptedMamQuery.size(), "Unexpected decrypted message count in MAM query result of " + bob.getConnection().getUser()); + assertEquals(body, decryptedMamQuery.get(decryptedMamQuery.size() - 1).getOmemoMessage().getBody(), + "Expected decrypted body of message retrieved via a MAM query to be equal to the original body that was sent (but it was not)."); } } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoManagerSetupHelper.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoManagerSetupHelper.java index 1a8093d63..7fdf53165 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoManagerSetupHelper.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoManagerSetupHelper.java @@ -21,8 +21,8 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; -import java.util.HashMap; import java.util.List; +import java.util.Map; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.SmackException.NotConnectedException; @@ -54,7 +54,7 @@ public class OmemoManagerSetupHelper { } alice.requestDeviceListUpdateFor(bob.getOwnJid()); - HashMap fingerprints = alice.getActiveFingerprints(bob.getOwnJid()); + Map fingerprints = alice.getActiveFingerprints(bob.getOwnJid()); for (OmemoDevice device : fingerprints.keySet()) { OmemoFingerprint fingerprint = fingerprints.get(device); @@ -67,7 +67,7 @@ public class OmemoManagerSetupHelper { SmackException.NoResponseException, CannotEstablishOmemoSessionException, CorruptedOmemoKeyException, XMPPException.XMPPErrorException, PubSubException.NotALeafNodeException, IOException { alice.requestDeviceListUpdateFor(bob.getOwnJid()); - HashMap fps1 = alice.getActiveFingerprints(bob.getOwnJid()); + Map fps1 = alice.getActiveFingerprints(bob.getOwnJid()); assertFalse(fps1.isEmpty()); assertAllDevicesAreUndecided(alice, fps1); @@ -75,7 +75,7 @@ public class OmemoManagerSetupHelper { trustAllIdentities(alice, bob); - HashMap fps2 = alice.getActiveFingerprints(bob.getOwnJid()); + Map fps2 = alice.getActiveFingerprints(bob.getOwnJid()); assertEquals(fps1.size(), fps2.size()); assertTrue(Maps.difference(fps1, fps2).areEqual()); @@ -95,28 +95,28 @@ public class OmemoManagerSetupHelper { return manager; } - public static void assertAllDevicesAreUndecided(OmemoManager manager, HashMap devices) { + public static void assertAllDevicesAreUndecided(OmemoManager manager, Map devices) { for (OmemoDevice device : devices.keySet()) { // All fingerprints MUST be neither decided, nor trusted. assertFalse(manager.isDecidedOmemoIdentity(device, devices.get(device))); } } - public static void assertAllDevicesAreUntrusted(OmemoManager manager, HashMap devices) { + public static void assertAllDevicesAreUntrusted(OmemoManager manager, Map devices) { for (OmemoDevice device : devices.keySet()) { // All fingerprints MUST be neither decided, nor trusted. assertFalse(manager.isTrustedOmemoIdentity(device, devices.get(device))); } } - public static void assertAllDevicesAreDecided(OmemoManager manager, HashMap devices) { + public static void assertAllDevicesAreDecided(OmemoManager manager, Map devices) { for (OmemoDevice device : devices.keySet()) { // All fingerprints MUST be neither decided, nor trusted. assertTrue(manager.isDecidedOmemoIdentity(device, devices.get(device))); } } - public static void assertAllDevicesAreTrusted(OmemoManager manager, HashMap devices) { + public static void assertAllDevicesAreTrusted(OmemoManager manager, Map devices) { for (OmemoDevice device : devices.keySet()) { // All fingerprints MUST be neither decided, nor trusted. assertTrue(manager.isTrustedOmemoIdentity(device, devices.get(device))); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/ReadOnlyDeviceIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/ReadOnlyDeviceIntegrationTest.java index 94c671962..b4daa8640 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/ReadOnlyDeviceIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/ReadOnlyDeviceIntegrationTest.java @@ -32,7 +32,9 @@ import org.jivesoftware.smackx.omemo.exceptions.UndecidedOmemoIdentityException; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; import org.igniterealtime.smack.inttest.TestNotPossibleException; import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; +import org.igniterealtime.smack.inttest.annotations.SpecificationReference; +@SpecificationReference(document = "XEP-0384", version = "0.3.0") public class ReadOnlyDeviceIntegrationTest extends AbstractTwoUsersOmemoIntegrationTest { public ReadOnlyDeviceIntegrationTest(SmackIntegrationTestEnvironment environment) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, TestNotPossibleException { diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/SessionRenegotiationIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/SessionRenegotiationIntegrationTest.java index c4a649911..0b3b15e79 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/SessionRenegotiationIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/SessionRenegotiationIntegrationTest.java @@ -24,7 +24,9 @@ import org.jivesoftware.smack.packet.MessageBuilder; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; import org.igniterealtime.smack.inttest.TestNotPossibleException; import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; +import org.igniterealtime.smack.inttest.annotations.SpecificationReference; +@SpecificationReference(document = "XEP-0384", version = "0.3.0") public class SessionRenegotiationIntegrationTest extends AbstractTwoUsersOmemoIntegrationTest { public SessionRenegotiationIntegrationTest(SmackIntegrationTestEnvironment environment) diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/ox/OXSecretKeyBackupIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/ox/OXSecretKeyBackupIntegrationTest.java index 60e3361ad..93f936785 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/ox/OXSecretKeyBackupIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/ox/OXSecretKeyBackupIntegrationTest.java @@ -38,6 +38,7 @@ import org.jivesoftware.smackx.ox.exception.MissingUserIdOnKeyException; import org.jivesoftware.smackx.ox.exception.NoBackupFoundException; import org.jivesoftware.smackx.ox.store.definition.OpenPgpStore; import org.jivesoftware.smackx.ox.store.filebased.FileBasedOpenPgpStore; +import org.jivesoftware.smackx.ox.util.OpenPgpPubSubUtil; import org.jivesoftware.smackx.pubsub.PubSubException; import org.bouncycastle.openpgp.PGPException; @@ -48,9 +49,11 @@ import org.igniterealtime.smack.inttest.TestNotPossibleException; import org.igniterealtime.smack.inttest.annotations.AfterClass; import org.igniterealtime.smack.inttest.annotations.BeforeClass; import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; +import org.igniterealtime.smack.inttest.annotations.SpecificationReference; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.protection.UnprotectedKeysProtector; +@SpecificationReference(document = "XEP-0374", version = "0.2.0") public class OXSecretKeyBackupIntegrationTest extends AbstractOpenPgpIntegrationTest { private static final String sessionId = StringUtils.randomString(10); @@ -101,7 +104,7 @@ public class OXSecretKeyBackupIntegrationTest extends AbstractOpenPgpIntegration org.apache.commons.io.FileUtils.deleteDirectory(beforePath); } - @SmackIntegrationTest + @SmackIntegrationTest(section = "5") public void test() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchProviderException, IOException, InterruptedException, PubSubException.NotALeafNodeException, SmackException.NoResponseException, SmackException.NotConnectedException, XMPPException.XMPPErrorException, @@ -120,38 +123,43 @@ public class OXSecretKeyBackupIntegrationTest extends AbstractOpenPgpIntegration assertNull(self.getSigningKeyFingerprint()); OpenPgpV4Fingerprint keyFingerprint = openPgpManager.generateAndImportKeyPair(alice); - assertEquals(keyFingerprint, self.getSigningKeyFingerprint()); - assertTrue(self.getSecretKeys().contains(keyFingerprint.getKeyId())); + try { + assertEquals(keyFingerprint, self.getSigningKeyFingerprint()); + assertTrue(self.getSecretKeys().contains(keyFingerprint.getKeyId())); - PGPSecretKeyRing beforeSec = beforeStore.getSecretKeyRing(alice, keyFingerprint); - assertNotNull(beforeSec); + PGPSecretKeyRing beforeSec = beforeStore.getSecretKeyRing(alice, keyFingerprint); + assertNotNull(beforeSec); - PGPPublicKeyRing beforePub = beforeStore.getPublicKeyRing(alice, keyFingerprint); - assertNotNull(beforePub); + PGPPublicKeyRing beforePub = beforeStore.getPublicKeyRing(alice, keyFingerprint); + assertNotNull(beforePub); - OpenPgpSecretKeyBackupPassphrase backupPassphrase = + OpenPgpSecretKeyBackupPassphrase backupPassphrase = openPgpManager.backupSecretKeyToServer(availableSecretKeys -> availableSecretKeys); - FileBasedOpenPgpStore afterStore = new FileBasedOpenPgpStore(afterPath); - afterStore.setKeyRingProtector(new UnprotectedKeysProtector()); - PainlessOpenPgpProvider afterProvider = new PainlessOpenPgpProvider(afterStore); - openPgpManager.setOpenPgpProvider(afterProvider); + FileBasedOpenPgpStore afterStore = new FileBasedOpenPgpStore(afterPath); + afterStore.setKeyRingProtector(new UnprotectedKeysProtector()); + PainlessOpenPgpProvider afterProvider = new PainlessOpenPgpProvider(afterStore); + openPgpManager.setOpenPgpProvider(afterProvider); - OpenPgpV4Fingerprint fingerprint = openPgpManager.restoreSecretKeyServerBackup(() -> backupPassphrase); + OpenPgpV4Fingerprint fingerprint = openPgpManager.restoreSecretKeyServerBackup(() -> backupPassphrase); - assertEquals(keyFingerprint, fingerprint); + assertEquals(keyFingerprint, fingerprint); - assertTrue(self.getSecretKeys().contains(keyFingerprint.getKeyId())); + assertTrue(self.getSecretKeys().contains(keyFingerprint.getKeyId())); - assertEquals(keyFingerprint, self.getSigningKeyFingerprint()); + assertEquals(keyFingerprint, self.getSigningKeyFingerprint()); - PGPSecretKeyRing afterSec = afterStore.getSecretKeyRing(alice, keyFingerprint); - assertNotNull(afterSec); - assertArrayEquals(beforeSec.getEncoded(), afterSec.getEncoded()); + PGPSecretKeyRing afterSec = afterStore.getSecretKeyRing(alice, keyFingerprint); + assertNotNull(afterSec); + assertArrayEquals(beforeSec.getEncoded(), afterSec.getEncoded()); - PGPPublicKeyRing afterPub = afterStore.getPublicKeyRing(alice, keyFingerprint); - assertNotNull(afterPub); - assertArrayEquals(beforePub.getEncoded(), afterPub.getEncoded()); + PGPPublicKeyRing afterPub = afterStore.getPublicKeyRing(alice, keyFingerprint); + assertNotNull(afterPub); + assertArrayEquals(beforePub.getEncoded(), afterPub.getEncoded()); + } finally { + OpenPgpPubSubUtil.deletePublicKeyNode(alicePepManager, keyFingerprint); + OpenPgpPubSubUtil.deletePubkeysListNode(alicePepManager); + } } } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/ox_im/OXInstantMessagingIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/ox_im/OXInstantMessagingIntegrationTest.java index 683bc199a..4467dde1f 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/ox_im/OXInstantMessagingIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/ox_im/OXInstantMessagingIntegrationTest.java @@ -33,17 +33,20 @@ import org.jivesoftware.smackx.ox.OpenPgpManager; import org.jivesoftware.smackx.ox.crypto.PainlessOpenPgpProvider; import org.jivesoftware.smackx.ox.element.SigncryptElement; import org.jivesoftware.smackx.ox.store.filebased.FileBasedOpenPgpStore; +import org.jivesoftware.smackx.ox.util.OpenPgpPubSubUtil; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; import org.igniterealtime.smack.inttest.TestNotPossibleException; import org.igniterealtime.smack.inttest.annotations.AfterClass; import org.igniterealtime.smack.inttest.annotations.BeforeClass; import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; +import org.igniterealtime.smack.inttest.annotations.SpecificationReference; import org.igniterealtime.smack.inttest.util.SimpleResultSyncPoint; import org.pgpainless.decryption_verification.OpenPgpMetadata; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.protection.UnprotectedKeysProtector; +@SpecificationReference(document = "XEP-0374", version = "0.2.0") public class OXInstantMessagingIntegrationTest extends AbstractOpenPgpIntegrationTest { private static final String sessionId = StringUtils.randomString(10); @@ -137,26 +140,33 @@ public class OXInstantMessagingIntegrationTest extends AbstractOpenPgpIntegratio aliceFingerprint = aliceOpenPgp.generateAndImportKeyPair(alice); bobFingerprint = bobOpenPgp.generateAndImportKeyPair(bob); - aliceOpenPgp.announceSupportAndPublish(); - bobOpenPgp.announceSupportAndPublish(); + try { + aliceOpenPgp.announceSupportAndPublish(); + bobOpenPgp.announceSupportAndPublish(); - OpenPgpContact bobForAlice = aliceOpenPgp.getOpenPgpContact(bob.asEntityBareJidIfPossible()); - OpenPgpContact aliceForBob = bobOpenPgp.getOpenPgpContact(alice.asEntityBareJidIfPossible()); + OpenPgpContact bobForAlice = aliceOpenPgp.getOpenPgpContact(bob.asEntityBareJidIfPossible()); + OpenPgpContact aliceForBob = bobOpenPgp.getOpenPgpContact(alice.asEntityBareJidIfPossible()); - bobForAlice.updateKeys(aliceConnection); + bobForAlice.updateKeys(aliceConnection); - assertFalse(bobForAlice.isTrusted(bobFingerprint)); - assertFalse(aliceForBob.isTrusted(aliceFingerprint)); + assertFalse(bobForAlice.isTrusted(bobFingerprint)); + assertFalse(aliceForBob.isTrusted(aliceFingerprint)); - bobForAlice.trust(bobFingerprint); - aliceForBob.trust(aliceFingerprint); + bobForAlice.trust(bobFingerprint); + aliceForBob.trust(aliceFingerprint); - assertTrue(bobForAlice.isTrusted(bobFingerprint)); - assertTrue(aliceForBob.isTrusted(aliceFingerprint)); + assertTrue(bobForAlice.isTrusted(bobFingerprint)); + assertTrue(aliceForBob.isTrusted(aliceFingerprint)); - aliceInstantMessaging.sendOxMessage(bobForAlice, body); + aliceInstantMessaging.sendOxMessage(bobForAlice, body); - bobReceivedMessage.waitForResult(timeout); + bobReceivedMessage.waitForResult(timeout); + } finally { + OpenPgpPubSubUtil.deletePublicKeyNode(alicePepManager, aliceFingerprint); + OpenPgpPubSubUtil.deletePubkeysListNode(alicePepManager); + OpenPgpPubSubUtil.deletePublicKeyNode(bobPepManager, bobFingerprint); + OpenPgpPubSubUtil.deletePubkeysListNode(bobPepManager); + } } } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/package-info.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/package-info.java index 7ebe86244..bce3b6064 120000 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/package-info.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/package-info.java @@ -1 +1 @@ -../../../../../../../smack-java8-full/src/main/java/org/jivesoftware/smackx/package-info.java \ No newline at end of file +../../../../../../../smack-java11-full/src/main/java/org/jivesoftware/smackx/package-info.java \ No newline at end of file diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/ping/PingIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/ping/PingIntegrationTest.java index 65c683093..a2cbce7ce 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/ping/PingIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/ping/PingIntegrationTest.java @@ -35,8 +35,10 @@ import org.jivesoftware.smack.XMPPConnection; import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; +import org.igniterealtime.smack.inttest.annotations.SpecificationReference; import org.jxmpp.jid.Jid; +@SpecificationReference(document = "XEP-0199", version = "2.0.1") public class PingIntegrationTest extends AbstractSmackIntegrationTest { public PingIntegrationTest(SmackIntegrationTestEnvironment environment) { diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/pubsub/PubSubIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/pubsub/PubSubIntegrationTest.java index 1890caf2d..d3f990a4a 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/pubsub/PubSubIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/pubsub/PubSubIntegrationTest.java @@ -34,8 +34,10 @@ import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; import org.igniterealtime.smack.inttest.TestNotPossibleException; import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; +import org.igniterealtime.smack.inttest.annotations.SpecificationReference; import org.jxmpp.jid.DomainBareJid; +@SpecificationReference(document = "XEP-0060", version = "1.26.0") public class PubSubIntegrationTest extends AbstractSmackIntegrationTest { private final PubSubManager pubSubManagerOne; @@ -82,21 +84,10 @@ public class PubSubIntegrationTest extends AbstractSmackIntegrationTest { } } - /** - - */ - /** * Asserts that an error is returned when a publish request to a node that is both * 'notification-only' as well as 'transient' contains an item element. * - *

    From XEP-0060 § 7.1.3.6:

    - *
    - * If the event type is notification + transient and the publisher provides an item, - * the service MUST bounce the publication request with a <bad-request/> error - * and a pubsub-specific error condition of <item-forbidden/>. - *
    - * * @throws NoResponseException if there was no response from the remote entity. * @throws XMPPErrorException if there was an XMPP error returned. * @throws NotConnectedException if the XMPP connection is not connected. @@ -104,7 +95,9 @@ public class PubSubIntegrationTest extends AbstractSmackIntegrationTest { * @see * 7.1.3.6 Request Does Not Match Configuration */ - @SmackIntegrationTest + @SmackIntegrationTest(section = "7.1.3.6", quote = + "If the event type is notification + transient and the publisher provides an item, the service MUST bounce " + + "the publication request with a error and a pubsub-specific error condition of .") public void transientNotificationOnlyNodeWithItemTest() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { final String nodename = "sinttest-transient-notificationonly-withitem-nodename-" + testRunId; final String itemId = "sinttest-transient-notificationonly-withitem-itemid-" + testRunId; @@ -120,7 +113,7 @@ public class PubSubIntegrationTest extends AbstractSmackIntegrationTest { // Add a dummy payload. If there is no payload, but just an item ID, then ejabberd will *not* return an error, // which I believe to be non-compliant behavior (although, granted, the XEP is not very clear about this). A user // which sends an empty item with ID to an node that is configured to be notification-only and transient probably - // does something wrong, as the item's ID will never appear anywhere. Hence it would be nice if the user would be + // does something wrong, as the item's ID will never appear anywhere. Hence, it would be nice if the user would be // made aware of this issue by returning an error. Sadly ejabberd does not do so. // See also https://github.com/processone/ejabberd/issues/2864#issuecomment-500741915 final StandardExtensionElement dummyPayload = StandardExtensionElement.builder("dummy-payload", @@ -132,9 +125,9 @@ public class PubSubIntegrationTest extends AbstractSmackIntegrationTest { Item item = new PayloadItem<>(itemId, dummyPayload); leafNode.publish(item); - }); - assertEquals(StanzaError.Type.MODIFY, e.getStanzaError().getType()); - assertNotNull(e.getStanzaError().getExtension("item-forbidden", "http://jabber.org/protocol/pubsub#errors")); + }, "Expected an error after publishing item " + itemId + " (but none occurred)."); + assertEquals(StanzaError.Type.MODIFY, e.getStanzaError().getType(), "Unexpected error type"); + assertNotNull(e.getStanzaError().getExtension("item-forbidden", "http://jabber.org/protocol/pubsub#errors"), "Expected error to contain 'item-forbidden', but it did not."); } finally { pubSubManagerOne.deleteNode(nodename); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/softwareInfo/SoftwareInfoIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/softwareInfo/SoftwareInfoIntegrationTest.java index 3bf111f53..f01371f10 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/softwareInfo/SoftwareInfoIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/softwareInfo/SoftwareInfoIntegrationTest.java @@ -31,10 +31,13 @@ import org.jivesoftware.smackx.softwareinfo.form.SoftwareInfoForm; import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; +import org.igniterealtime.smack.inttest.annotations.AfterClass; import org.igniterealtime.smack.inttest.annotations.BeforeClass; import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; +import org.igniterealtime.smack.inttest.annotations.SpecificationReference; import org.igniterealtime.smack.inttest.util.IntegrationTestRosterUtil; +@SpecificationReference(document = "XEP-0232", version = "0.3") public class SoftwareInfoIntegrationTest extends AbstractSmackIntegrationTest { public final SoftwareInfoManager sim1; @@ -52,6 +55,11 @@ public class SoftwareInfoIntegrationTest extends AbstractSmackIntegrationTest { IntegrationTestRosterUtil.ensureBothAccountsAreSubscribedToEachOther(conOne, conTwo, timeout); } + @AfterClass + public void cleanUp() throws Exception { + IntegrationTestRosterUtil.ensureBothAccountsAreNotInEachOthersRoster(conOne, conTwo); + } + @SmackIntegrationTest public void test() throws Exception { SoftwareInfoForm softwareInfoSent = createSoftwareInfoForm(); @@ -62,7 +70,8 @@ public class SoftwareInfoIntegrationTest extends AbstractSmackIntegrationTest { } }); SoftwareInfoForm softwareInfoFormReceived = sim2.fromJid(conOne.getUser()); - assertEquals(softwareInfoSent, softwareInfoFormReceived); + assertEquals(softwareInfoSent, softwareInfoFormReceived, + "Expected " + conOne.getUser() + "'s software version info as received by " + conTwo.getUser() + " to be equal to what " + conOne.getUser() + " publishes (but it is not)."); } private static SoftwareInfoForm createSoftwareInfoForm() throws URISyntaxException { diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/usertune/UserTuneIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/usertune/UserTuneIntegrationTest.java index 653bded72..e204031c4 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/usertune/UserTuneIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/usertune/UserTuneIntegrationTest.java @@ -17,7 +17,6 @@ package org.jivesoftware.smackx.usertune; import java.net.URI; -import java.util.concurrent.TimeoutException; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.SmackException.NotLoggedInException; @@ -32,10 +31,12 @@ import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; import org.igniterealtime.smack.inttest.annotations.AfterClass; import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; +import org.igniterealtime.smack.inttest.annotations.SpecificationReference; import org.igniterealtime.smack.inttest.util.IntegrationTestRosterUtil; import org.igniterealtime.smack.inttest.util.SimpleResultSyncPoint; import org.junit.jupiter.api.Assertions; +@SpecificationReference(document = "XEP-0118", version = "1.3.0") public class UserTuneIntegrationTest extends AbstractSmackIntegrationTest { private final UserTuneManager utm1; @@ -97,6 +98,7 @@ public class UserTuneIntegrationTest extends AbstractSmackIntegrationTest { Assertions.assertNotNull(result, "Expected to receive a PEP notification, but did not."); } finally { unregisterListener(utm2, userTuneListener); + IntegrationTestRosterUtil.ensureBothAccountsAreNotInEachOthersRoster(conOne, conTwo); } } @@ -138,16 +140,10 @@ public class UserTuneIntegrationTest extends AbstractSmackIntegrationTest { registerListenerAndWait(utm2, ServiceDiscoveryManager.getInstanceFor(conTwo), userTuneListener); // Wait for the data to be received. - try { - Object result = userTuneReceived.waitForResult(timeout); - - // Explicitly assert the success case. - Assertions.assertNotNull(result, "Expected to receive a PEP notification, but did not."); - } catch (TimeoutException e) { - Assertions.fail("Expected to receive a PEP notification, but did not."); - } + assertResult(userTuneReceived, "Expected " + conTwo.getUser() + " to receive a PEP notification from " + conOne.getUser() + ", but did not."); } finally { unregisterListener(utm2, userTuneListener); + IntegrationTestRosterUtil.ensureBothAccountsAreNotInEachOthersRoster(conOne, conTwo); } } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/xdata/FormTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/xdata/FormTest.java index 64b4db597..9e127b85e 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/xdata/FormTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/xdata/FormTest.java @@ -128,28 +128,29 @@ public class FormTest extends AbstractSmackIntegrationTest { completedForm.setAnswer("time", true); completedForm.setAnswer("age", 20); // Create a new message to send with the completed form - msg2 = StanzaBuilder.buildMessage() + Message msg3 = StanzaBuilder.buildMessage() .to(conOne.getUser().asBareJid()) - .setThread(msg.getThread()) + .setThread(msg2.getThread()) .ofType(Message.Type.chat) .setBody("To enter a case please fill out this form and send it back to me") // Add the completed form to the message .addExtension(completedForm.getDataFormToSubmit()) .build(); // Send the message with the completed form - conTwo.sendStanza(msg2); + conTwo.sendStanza(msg3); // Get the message with the completed form - Message msg3 = collector.nextResult(); - assertNotNull(msg3, "Message not found"); + Message msg4 = collector.nextResult(); + assertNotNull(msg4, "Message not found"); // Retrieve the completed form - final DataForm completedForm2 = DataForm.from(msg3); + final DataForm completedForm2 = DataForm.from(msg4); assertNotNull(completedForm2); assertNotNull(completedForm2.getField("name")); assertNotNull(completedForm2.getField("description")); assertEquals( - completedForm2.getField("name").getValues().get(0).toString(), - "Credit card number invalid"); + "Credit card number invalid", + completedForm2.getField("name").getValues().get(0).toString() + ); assertNotNull(completedForm2.getField("time")); assertNotNull(completedForm2.getField("age")); assertEquals("20", completedForm2.getField("age").getValues().get(0).toString(), "The age is bad"); diff --git a/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/ConfigurationTest.java b/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/ConfigurationTest.java new file mode 100644 index 000000000..727b8a756 --- /dev/null +++ b/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/ConfigurationTest.java @@ -0,0 +1,98 @@ +/** + * + * Copyright 2024 Guus der Kinderen + * + * 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.igniterealtime.smack.inttest; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +/** + * Verifies the functionality that's implemented in {@link Configuration}. + */ +public class ConfigurationTest { + @Test + public void testNormalizeXepUpperCaseNoSeperator() { + // Setup test fixture. + final String input = "XEP0001"; + + // Execute system under test. + final String output = Configuration.normalizeSpecification(input); + + // Verify results. + assertEquals("XEP0001", output); + } + + @Test + public void testNormalizeXepLowerCaseNoSeperator() { + // Setup test fixture. + final String input = "xep0001"; + + // Execute system under test. + final String output = Configuration.normalizeSpecification(input); + + // Verify results. + assertEquals("XEP0001", output); + } + + @Test + public void testNormalizeXepUpperCaseDash() { + // Setup test fixture. + final String input = "XEP-0001"; + + // Execute system under test. + final String output = Configuration.normalizeSpecification(input); + + // Verify results. + assertEquals("XEP0001", output); + } + + @Test + public void testNormalizeXepLowerCaseDash() { + // Setup test fixture. + final String input = "xep-0001"; + + // Execute system under test. + final String output = Configuration.normalizeSpecification(input); + + // Verify results. + assertEquals("XEP0001", output); + } + + @Test + public void testNormalizeXepUpperCaseSpace() { + // Setup test fixture. + final String input = "XEP 0001"; + + // Execute system under test. + final String output = Configuration.normalizeSpecification(input); + + // Verify results. + assertEquals("XEP0001", output); + } + + @Test + public void testNormalizeXepLowerCaseSpace() { + // Setup test fixture. + final String input = "xep 0001"; + + // Execute system under test. + final String output = Configuration.normalizeSpecification(input); + + // Verify results. + assertEquals("XEP0001", output); + } +} diff --git a/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFrameWorkTest.java b/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFrameWorkTest.java index 48b827770..fbdfa8cef 100644 --- a/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFrameWorkTest.java +++ b/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFrameWorkTest.java @@ -31,26 +31,26 @@ import org.junit.jupiter.api.Test; public class SmackIntegrationTestFrameWorkTest { private static class ValidLowLevelList { - @SuppressWarnings("unused") + @SuppressWarnings({"unused", "MethodCanBeStatic"}) public void test(List connections) { } } private static class InvalidLowLevelList { - @SuppressWarnings("unused") + @SuppressWarnings({"unused", "MethodCanBeStatic"}) public void test(List connections, boolean invalid) { } } private static class ValidLowLevelVarargs { - @SuppressWarnings("unused") + @SuppressWarnings({"unused", "MethodCanBeStatic"}) public void test(AbstractXMPPConnection connectionOne, AbstractXMPPConnection connectionTwo, AbstractXMPPConnection connectionThree) { } } private static class InvalidLowLevelVarargs { - @SuppressWarnings("unused") + @SuppressWarnings({"unused", "MethodCanBeStatic"}) public void test(AbstractXMPPConnection connectionOne, Integer invalid, AbstractXMPPConnection connectionTwo, AbstractXMPPConnection connectionThree) { } @@ -97,7 +97,7 @@ public class SmackIntegrationTestFrameWorkTest { } private static class ValidUnconnectedConnectionSource { - @SuppressWarnings("unused") + @SuppressWarnings({"unused", "MethodCanBeStatic"}) public void test(AbstractSmackLowLevelIntegrationTest.UnconnectedConnectionSource source) { } } diff --git a/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/SmackIntegrationTestXmppConnectionManagerTest.java b/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/SmackIntegrationTestXmppConnectionManagerTest.java index 7a2de6097..d8aa16015 100644 --- a/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/SmackIntegrationTestXmppConnectionManagerTest.java +++ b/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/SmackIntegrationTestXmppConnectionManagerTest.java @@ -43,7 +43,7 @@ public class SmackIntegrationTestXmppConnectionManagerTest { ModularXmppClientToServerConnection.class, ModularXmppClientToServerConnectionConfiguration.class, ModularXmppClientToServerConnectionConfiguration.Builder.class) - .applyExtraConfguration(b -> b.removeAllModules().addModule(XmppTcpTransportModuleDescriptor.class)) + .applyExtraConfiguration(b -> b.removeAllModules().addModule(XmppTcpTransportModuleDescriptor.class)) .build(); Configuration sinttestConfiguration = Configuration.builder().setService("example.org").build(); diff --git a/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/util/MultiResultSyncPointTest.java b/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/util/MultiResultSyncPointTest.java new file mode 100644 index 000000000..5a4db37c4 --- /dev/null +++ b/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/util/MultiResultSyncPointTest.java @@ -0,0 +1,108 @@ +/** + * + * Copyright 2024 Guus der Kinderen + * + * 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.igniterealtime.smack.inttest.util; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.List; +import java.util.concurrent.BrokenBarrierException; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.TimeoutException; + +import org.jivesoftware.smack.util.Async; + +import org.junit.jupiter.api.Test; + +public class MultiResultSyncPointTest { + @Test + public void testResultSyncPoint() throws Exception { + final String result1 = "r1"; + final String result2 = "r2"; + final CyclicBarrier barrier = new CyclicBarrier(2); + final MultiResultSyncPoint rsp = new MultiResultSyncPoint<>(2); + Async.go(new Async.ThrowingRunnable() { + @Override + public void runOrThrow() throws InterruptedException, BrokenBarrierException { + barrier.await(); + rsp.signal(result1); + rsp.signal(result2); + } + }); + barrier.await(); + List receivedResult = rsp.waitForResults(60 * 1000); + assertTrue(receivedResult.contains(result1)); + assertTrue(receivedResult.contains(result2)); + } + + @Test + public void exceptionTestResultSyncPoint() throws Exception { + final CyclicBarrier barrier = new CyclicBarrier(2); + final ResultSyncPoint rsp = new ResultSyncPoint<>(); + Async.go(new Async.ThrowingRunnable() { + @Override + public void runOrThrow() throws InterruptedException, BrokenBarrierException { + barrier.await(); + rsp.signal(new MultiResultSyncPointTest.TestException()); + } + }); + barrier.await(); + assertThrows(MultiResultSyncPointTest.TestException.class, () -> rsp.waitForResult(60 * 1000)); + } + + @Test + public void testTimeout() throws Exception { + final MultiResultSyncPoint rsp = new MultiResultSyncPoint<>(2); + try { + rsp.waitForResults(100); + fail("A timeout exception should have been thrown."); + } catch (TimeoutException e) { + // Expected + } + } + + @Test + public void testTimeoutWithOneResult() throws Exception { + final String result1 = "partial"; + final CyclicBarrier barrier = new CyclicBarrier(2); + final MultiResultSyncPoint rsp = new MultiResultSyncPoint<>(2); + Async.go(new Async.ThrowingRunnable() { + @Override + public void runOrThrow() throws InterruptedException, BrokenBarrierException { + barrier.await(); + rsp.signal(result1); + } + }); + barrier.await(); + try { + rsp.waitForResults(100); + fail("A timeout exception should have been thrown."); + } catch (TimeoutException e) { + // Expected + } + } + + private static class TestException extends Exception { + + /** + * + */ + private static final long serialVersionUID = 1L; + + } +} diff --git a/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/util/ResultSyncPointTest.java b/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/util/ResultSyncPointTest.java index 589880204..c6e22bd72 100644 --- a/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/util/ResultSyncPointTest.java +++ b/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/util/ResultSyncPointTest.java @@ -18,9 +18,11 @@ package org.igniterealtime.smack.inttest.util; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.fail; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.TimeoutException; import org.jivesoftware.smack.util.Async; @@ -60,6 +62,17 @@ public class ResultSyncPointTest { assertThrows(TestException.class, () -> rsp.waitForResult(60 * 1000)); } + @Test + public void testTimeout() throws Exception { + final MultiResultSyncPoint rsp = new MultiResultSyncPoint<>(2); + try { + rsp.waitForResults(100); + fail("A timeout exception should have been thrown."); + } catch (TimeoutException e) { + // Expected + } + } + private static class TestException extends Exception { /** diff --git a/smack-java8-full/build.gradle b/smack-java11-full/build.gradle similarity index 91% rename from smack-java8-full/build.gradle rename to smack-java11-full/build.gradle index 7607d3c02..4f4bf0652 100644 --- a/smack-java8-full/build.gradle +++ b/smack-java11-full/build.gradle @@ -1,3 +1,7 @@ +plugins { + id 'org.igniterealtime.smack.java-common-conventions' +} + description = """\ Full Smack library for Java SE.""" @@ -6,7 +10,7 @@ dependencies { api project(':smack-debug') api project(':smack-experimental') api project(':smack-extensions') - api project(':smack-java8') + api project(':smack-java11') api project(':smack-legacy') api project(':smack-omemo') api project(':smack-openpgp') @@ -44,6 +48,7 @@ task convertModularXmppClientToServerConnectionStateGraphDotToPng(type: Exec) { executable 'dot' args "-Tpng", "-o", "${outputs.files.first()}", "${inputs.files.first()}" } +copyJavadocDocFiles.dependsOn convertModularXmppClientToServerConnectionStateGraphDotToPng task cleanGenerateFiles(type: Delete) { delete 'src/javadoc/org/jivesoftware/smack/full/doc-files/ModularXmppClientToServerConnectionStateGraph.dot', 'src/javadoc/org/jivesoftware/smack/full/doc-files/ModularXmppClientToServerConnectionStateGraph.png' diff --git a/smack-java8-full/src/javadoc/org/jivesoftware/smack/full/doc-files/.gitignore b/smack-java11-full/src/javadoc/org/jivesoftware/smack/full/doc-files/.gitignore similarity index 100% rename from smack-java8-full/src/javadoc/org/jivesoftware/smack/full/doc-files/.gitignore rename to smack-java11-full/src/javadoc/org/jivesoftware/smack/full/doc-files/.gitignore diff --git a/smack-java8-full/src/main/java/org/jivesoftware/smack/full/ModularXmppClientToServerConnectionTool.java b/smack-java11-full/src/main/java/org/jivesoftware/smack/full/ModularXmppClientToServerConnectionTool.java similarity index 100% rename from smack-java8-full/src/main/java/org/jivesoftware/smack/full/ModularXmppClientToServerConnectionTool.java rename to smack-java11-full/src/main/java/org/jivesoftware/smack/full/ModularXmppClientToServerConnectionTool.java diff --git a/smack-java8-full/src/main/java/org/jivesoftware/smack/full/package-info.java b/smack-java11-full/src/main/java/org/jivesoftware/smack/full/package-info.java similarity index 100% rename from smack-java8-full/src/main/java/org/jivesoftware/smack/full/package-info.java rename to smack-java11-full/src/main/java/org/jivesoftware/smack/full/package-info.java diff --git a/smack-java8-full/src/main/java/org/jivesoftware/smackx/SmackExtensions.java b/smack-java11-full/src/main/java/org/jivesoftware/smackx/SmackExtensions.java similarity index 94% rename from smack-java8-full/src/main/java/org/jivesoftware/smackx/SmackExtensions.java rename to smack-java11-full/src/main/java/org/jivesoftware/smackx/SmackExtensions.java index 8ec3ad057..186562bba 100644 --- a/smack-java8-full/src/main/java/org/jivesoftware/smackx/SmackExtensions.java +++ b/smack-java11-full/src/main/java/org/jivesoftware/smackx/SmackExtensions.java @@ -18,7 +18,7 @@ package org.jivesoftware.smackx; /** * This is just a dummy class, please head over to {@link org.jivesoftware.smackx} for more information on Smack - * extensions. The dummy class causes javadoc generate the HTML for smackx.pacakge-info.java, which would otherwise be + * extensions. The dummy class causes javadoc generate the HTML for smackx.package-info.java, which would otherwise be * not generated, as org.jivesoftware.smackx is an empty package (see * JDK-4492654). */ diff --git a/smack-java8-full/src/main/java/org/jivesoftware/smackx/package-info.java b/smack-java11-full/src/main/java/org/jivesoftware/smackx/package-info.java similarity index 94% rename from smack-java8-full/src/main/java/org/jivesoftware/smackx/package-info.java rename to smack-java11-full/src/main/java/org/jivesoftware/smackx/package-info.java index 8b3550028..0769f65ba 100644 --- a/smack-java8-full/src/main/java/org/jivesoftware/smackx/package-info.java +++ b/smack-java11-full/src/main/java/org/jivesoftware/smackx/package-info.java @@ -71,7 +71,7 @@ * Extended Stanza Addressing * XEP-0033 * - * Allows to include headers in stanzas in order to specifiy multiple recipients or sub-addresses. + * Allows to include headers in stanzas in order to specify multiple recipients or sub-addresses. * * * Multi User Chat @@ -206,6 +206,18 @@ * Transfer files between two users over XMPP. * * + * URL Address Information + * XEP-0103 + * {@link org.jivesoftware.smackx.urldata.element} + * Provide information about an Uniform Resource Locator (URL), and a protocol signaling retrieval states. + * + * + * HTTP Scheme for URL Data + * XEP-0104 + * + * A schema description for detailed information about HTTP URLs. + * + * * User Mood * XEP-0107 * @@ -358,6 +370,12 @@ * Allows sending a MUC invitation directly from the user to the contact with mediation by the room. * * + * Jingle Content Thumbnails + * XEP-0264 + * {@link org.jivesoftware.smackx.thumbnails.element} + * Defines a way for a client to supply a preview image for Jingle content. + * + * * Message Carbons * XEP-0280 * {@link org.jivesoftware.smackx.carbons} @@ -459,7 +477,7 @@ * Data Forms Geolocation Element * XEP-0350 * - * Allows to include XEP-0080 gelocation data in XEP-0004 data forms. + * Allows to include XEP-0080 geolocation data in XEP-0004 data forms. * * * Client State Indication @@ -493,7 +511,7 @@ * * * References - * XEP-0372 + * XEP-0372 * * Add references like mentions or external data to stanzas. * @@ -571,6 +589,12 @@ * Declare body elements of a message as ignorable fallback for naive legacy clients. * * + * File metadata element + * XEP-0446 + * {@link org.jivesoftware.smackx.file_metadata.element} + * Defines a generic file metadata element to be used in other specifications. + * + * * Google GCM JSON payload * * @@ -584,12 +608,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-java8-full/src/test/java/org/jivesoftware/smack/full/ExtensionElementQNameDeclaredTest.java b/smack-java11-full/src/test/java/org/jivesoftware/smack/full/ExtensionElementQNameDeclaredTest.java similarity index 100% rename from smack-java8-full/src/test/java/org/jivesoftware/smack/full/ExtensionElementQNameDeclaredTest.java rename to smack-java11-full/src/test/java/org/jivesoftware/smack/full/ExtensionElementQNameDeclaredTest.java diff --git a/smack-java8-full/src/test/java/org/jivesoftware/smack/full/ModularXmppClientToServerConnectionStateGraphTest.java b/smack-java11-full/src/test/java/org/jivesoftware/smack/full/ModularXmppClientToServerConnectionStateGraphTest.java similarity index 100% rename from smack-java8-full/src/test/java/org/jivesoftware/smack/full/ModularXmppClientToServerConnectionStateGraphTest.java rename to smack-java11-full/src/test/java/org/jivesoftware/smack/full/ModularXmppClientToServerConnectionStateGraphTest.java diff --git a/smack-java8-full/src/test/resources/state-graph.dot b/smack-java11-full/src/test/resources/state-graph.dot similarity index 100% rename from smack-java8-full/src/test/resources/state-graph.dot rename to smack-java11-full/src/test/resources/state-graph.dot diff --git a/smack-java8/build.gradle b/smack-java11/build.gradle similarity index 67% rename from smack-java8/build.gradle rename to smack-java11/build.gradle index d26aa5ce3..d3d157ad1 100644 --- a/smack-java8/build.gradle +++ b/smack-java11/build.gradle @@ -1,7 +1,11 @@ +plugins { + id 'org.igniterealtime.smack.java-common-conventions' +} + description = """\ -Smack for Java7 (or higher). +Smack for Java 11 (or higher). This is a pseudo-artifact that pulls all the required dependencies to -run Smack on Java 7 (or higher) JVMs. Usually you want to add additional +run Smack on Java 11 (or higher) JVMs. Usually you want to add additional dependencies to smack-tcp, smack-extensions and smack-experimental.""" dependencies { diff --git a/smack-java8/src/main/java/org/jivesoftware/smack/java7/Java7SmackInitializer.java b/smack-java11/src/main/java/org/jivesoftware/smack/java7/Java7SmackInitializer.java similarity index 100% rename from smack-java8/src/main/java/org/jivesoftware/smack/java7/Java7SmackInitializer.java rename to smack-java11/src/main/java/org/jivesoftware/smack/java7/Java7SmackInitializer.java diff --git a/smack-java8/src/main/java/org/jivesoftware/smack/java7/XmppHostnameVerifier.java b/smack-java11/src/main/java/org/jivesoftware/smack/java7/XmppHostnameVerifier.java similarity index 98% rename from smack-java8/src/main/java/org/jivesoftware/smack/java7/XmppHostnameVerifier.java rename to smack-java11/src/main/java/org/jivesoftware/smack/java7/XmppHostnameVerifier.java index 16e1c5668..a03cd097a 100644 --- a/smack-java8/src/main/java/org/jivesoftware/smack/java7/XmppHostnameVerifier.java +++ b/smack-java11/src/main/java/org/jivesoftware/smack/java7/XmppHostnameVerifier.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015 Florian Schmaus + * Copyright 2015-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. @@ -23,8 +23,8 @@ import java.security.Principal; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; +import java.util.ArrayList; import java.util.Collection; -import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.logging.Level; @@ -135,7 +135,7 @@ public class XmppHostnameVerifier implements HostnameVerifier { private static void matchDns(String name, X509Certificate cert) throws CertificateException { Collection> subjAltNames = cert.getSubjectAlternativeNames(); if (subjAltNames != null) { - List nonMatchingDnsAltnames = new LinkedList<>(); + List nonMatchingDnsAltnames = new ArrayList<>(); for (List san : subjAltNames) { if (((Integer) san.get(0)).intValue() != ALTNAME_DNS) { continue; @@ -253,7 +253,7 @@ public class XmppHostnameVerifier implements HostnameVerifier { if (subjectAlternativeNames == null) { throw new CertificateException("No subject alternative names present"); } - List nonMatchingIpAltnames = new LinkedList<>(); + List nonMatchingIpAltnames = new ArrayList<>(); for (List san : subjectAlternativeNames) { if (((Integer) san.get(0)).intValue() != ALTNAME_IP) { continue; diff --git a/smack-java8/src/main/java/org/jivesoftware/smack/java7/package-info.java b/smack-java11/src/main/java/org/jivesoftware/smack/java7/package-info.java similarity index 100% rename from smack-java8/src/main/java/org/jivesoftware/smack/java7/package-info.java rename to smack-java11/src/main/java/org/jivesoftware/smack/java7/package-info.java diff --git a/smack-java8/src/main/java/org/jivesoftware/smack/util/stringencoder/java7/Java7Base64Encoder.java b/smack-java11/src/main/java/org/jivesoftware/smack/util/stringencoder/java7/Java7Base64Encoder.java similarity index 100% rename from smack-java8/src/main/java/org/jivesoftware/smack/util/stringencoder/java7/Java7Base64Encoder.java rename to smack-java11/src/main/java/org/jivesoftware/smack/util/stringencoder/java7/Java7Base64Encoder.java diff --git a/smack-java8/src/main/java/org/jivesoftware/smack/util/stringencoder/java7/Java7Base64UrlSafeEncoder.java b/smack-java11/src/main/java/org/jivesoftware/smack/util/stringencoder/java7/Java7Base64UrlSafeEncoder.java similarity index 100% rename from smack-java8/src/main/java/org/jivesoftware/smack/util/stringencoder/java7/Java7Base64UrlSafeEncoder.java rename to smack-java11/src/main/java/org/jivesoftware/smack/util/stringencoder/java7/Java7Base64UrlSafeEncoder.java diff --git a/smack-java8/src/main/java/org/jivesoftware/smack/util/stringencoder/java7/package-info.java b/smack-java11/src/main/java/org/jivesoftware/smack/util/stringencoder/java7/package-info.java similarity index 100% rename from smack-java8/src/main/java/org/jivesoftware/smack/util/stringencoder/java7/package-info.java rename to smack-java11/src/main/java/org/jivesoftware/smack/util/stringencoder/java7/package-info.java diff --git a/smack-java8/src/main/java/org/jivesoftware/smack/util/stringencoder/package-info.java b/smack-java11/src/main/java/org/jivesoftware/smack/util/stringencoder/package-info.java similarity index 100% rename from smack-java8/src/main/java/org/jivesoftware/smack/util/stringencoder/package-info.java rename to smack-java11/src/main/java/org/jivesoftware/smack/util/stringencoder/package-info.java 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-jingle-old/build.gradle b/smack-jingle-old/build.gradle index 5651864f2..63e732bee 100644 --- a/smack-jingle-old/build.gradle +++ b/smack-jingle-old/build.gradle @@ -1,3 +1,7 @@ +plugins { + id 'org.igniterealtime.smack.java-common-conventions' +} + description = """\ Smack Jingle API. Warning: This API is beta, outdated and currenlty unmaintained.""" diff --git a/smack-jingle-old/src/integration-test/java/org/jivesoftware/smackx/jingle/nat/STUNResolverTest.java b/smack-jingle-old/src/integration-test/java/org/jivesoftware/smackx/jingle/nat/STUNResolverTest.java index 5d9425a92..febac9374 100644 --- a/smack-jingle-old/src/integration-test/java/org/jivesoftware/smackx/jingle/nat/STUNResolverTest.java +++ b/smack-jingle-old/src/integration-test/java/org/jivesoftware/smackx/jingle/nat/STUNResolverTest.java @@ -99,7 +99,7 @@ public class STUNResolverTest extends SmackTestCase { stunResolver.addCandidate(cand3); stunResolver.addCandidate(cand4); - assertEquals(stunResolver.getPreferredCandidate(), candH); + assertEquals(candH, stunResolver.getPreferredCandidate()); } /** @@ -127,7 +127,7 @@ public class STUNResolverTest extends SmackTestCase { iceResolver.addCandidate(cand3); iceResolver.addCandidate(cand4); - assertEquals(iceResolver.getPreferredCandidate(), candH); + assertEquals(candH, iceResolver.getPreferredCandidate()); } /** diff --git a/smack-jingle-old/src/integration-test/java/org/jivesoftware/smackx/jingle/nat/TransportCandidateTest.java b/smack-jingle-old/src/integration-test/java/org/jivesoftware/smackx/jingle/nat/TransportCandidateTest.java index 36830e911..763d52877 100644 --- a/smack-jingle-old/src/integration-test/java/org/jivesoftware/smackx/jingle/nat/TransportCandidateTest.java +++ b/smack-jingle-old/src/integration-test/java/org/jivesoftware/smackx/jingle/nat/TransportCandidateTest.java @@ -65,7 +65,7 @@ public class TransportCandidateTest extends SmackTestCase { candList.add(cand4); Collections.sort(candList); - assertEquals(candList.get(candList.size() - 1), candH); + assertEquals(candH, candList.get(candList.size() - 1)); } protected int getMaxConnections() { diff --git a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/JingleManager.java b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/JingleManager.java index acd9a5796..f4ad1c0a8 100644 --- a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/JingleManager.java +++ b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/JingleManager.java @@ -540,7 +540,7 @@ public class JingleManager implements JingleSessionListener { // } /** * When the session request is acceptable, this method should be invoked. It - * will create an JingleSession which allows the negotiation to procede. + * will create an JingleSession which allows the negotiation to proceed. * * @param request the remote request that is being accepted. * @return the session which manages the rest of the negotiation. @@ -560,7 +560,7 @@ public class JingleManager implements JingleSessionListener { /** * When the session request is acceptable, this method should be invoked. It - * will create an JingleSession which allows the negotiation to procede. + * will create an JingleSession which allows the negotiation to proceed. * This method use JingleMediaManager to select the supported Payload types. * * @param request the remote request that is being accepted. diff --git a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/JingleNegotiator.java b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/JingleNegotiator.java index 656faad25..d944679b8 100644 --- a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/JingleNegotiator.java +++ b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/JingleNegotiator.java @@ -221,7 +221,7 @@ public abstract class JingleNegotiator { * <transport> * * This way, each segment of a Jingle stanza has a corresponding negotiator that know how to deal with that - * part of the Jingle packet. It also allows us to support Jingle packets of arbitraty complexity. + * part of the Jingle packet. It also allows us to support Jingle packets of arbitrary complexity. * * Each parent calls dispatchIncomingPacket for each of its children. The children then pass back a List of * results that will get sent when we reach the top level negotiator (JingleSession). diff --git a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/JingleSession.java b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/JingleSession.java index dabc962b0..cb141e837 100644 --- a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/JingleSession.java +++ b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/JingleSession.java @@ -431,7 +431,7 @@ public final class JingleSession extends JingleNegotiator implements MediaReceiv /** * Complete and send a packet. Complete all the null fields in a Jingle - * reponse, using the session information we have. + * response, using the session information we have. * * @param jout * the Jingle stanza we want to complete and send @@ -445,7 +445,7 @@ public final class JingleSession extends JingleNegotiator implements MediaReceiv /** * Complete and send a packet. Complete all the null fields in a Jingle - * reponse, using the session information we have or some info from the + * response, using the session information we have or some info from the * incoming packet. * * @param iq The Jingle stanza we are responding to @@ -1097,7 +1097,7 @@ public final class JingleSession extends JingleNegotiator implements MediaReceiv } /** - * This is the starting point for intitiating a new session. + * This is the starting point for initiating a new session. * * @throws IllegalStateException if an illegal state was encountered * @throws SmackException if Smack detected an exceptional situation. diff --git a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/JingleSessionState.java b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/JingleSessionState.java index a1bc7263b..5a1f7660a 100644 --- a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/JingleSessionState.java +++ b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/JingleSessionState.java @@ -56,7 +56,7 @@ public abstract class JingleSessionState { /** * Process an incoming Jingle Packet. - * When you look at the GoF State pattern this method roughly corresponds to example on p310: ProcessOctect() + * When you look at the GoF State pattern this method roughly corresponds to example on p310: ProcessOctet() * * @param session the jingle session. * @param jingle the jingle stanza. diff --git a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/listeners/CreatedJingleSessionListener.java b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/listeners/CreatedJingleSessionListener.java index 294e321cd..dda67f72e 100644 --- a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/listeners/CreatedJingleSessionListener.java +++ b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/listeners/CreatedJingleSessionListener.java @@ -19,7 +19,7 @@ package org.jivesoftware.smackx.jingleold.listeners; import org.jivesoftware.smackx.jingleold.JingleSession; /** - * Inteface used to dispatch a event when a Jingle session is created. + * Interface used to dispatch an event when a Jingle session is created. * * @author Thiago Camargo */ diff --git a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/listeners/JingleSessionListener.java b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/listeners/JingleSessionListener.java index fb754e489..768cc42c9 100644 --- a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/listeners/JingleSessionListener.java +++ b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/listeners/JingleSessionListener.java @@ -33,7 +33,7 @@ public interface JingleSessionListener extends JingleListener { * Notification that the session has been established. Arguments specify * the payload type and transport to use. * - * @param pt the Payload tyep to use + * @param pt the Payload type to use * @param remoteCandidate the remote candidate to use for connecting to the remote * service. * @param localCandidate the local candidate where we must listen for connections diff --git a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/media/JingleMediaSession.java b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/media/JingleMediaSession.java index 2956a265a..7df3c2f06 100644 --- a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/media/JingleMediaSession.java +++ b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/media/JingleMediaSession.java @@ -25,7 +25,7 @@ import org.jivesoftware.smackx.jingleold.nat.TransportCandidate; /** * Public Abstract Class provides a clear interface between Media Session and Jingle API. *

    - * When a Jingle Session is fully stablished, we will have a Payload Type and two transport candidates defined for it. + * When a Jingle Session is fully established, we will have a Payload Type and two transport candidates defined for it. * Smack Jingle API don't implement Media Transmit and Receive methods. * But provides an interface to let the user implements it using another API. For instance: JMF. *

    @@ -153,7 +153,7 @@ public abstract class JingleMediaSession { public abstract void startReceive(); /** - * Set transmit activity. If the active is true, the instance should trasmit. + * Set transmit activity. If the active is true, the instance should transmit. * If it is set to false, the instance should pause transmit. * * @param active TODO javadoc me please @@ -173,7 +173,7 @@ public abstract class JingleMediaSession { /** * Called when new Media is received. * - * @param participant the particpant. + * @param participant the participant. */ public void mediaReceived(String participant) { for (MediaReceivedListener mediaReceivedListener : mediaReceivedListeners) { diff --git a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/media/PayloadType.java b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/media/PayloadType.java index c7dd7b8e0..08675d8b4 100644 --- a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/media/PayloadType.java +++ b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/media/PayloadType.java @@ -322,7 +322,7 @@ public class PayloadType { } /** - * Set tha sampling clockRate for a playload type. + * Set tha sampling clockRate for a payload type. * * @param rate The sampling clockRate */ diff --git a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/mediaimpl/JMFInit.java b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/mediaimpl/JMFInit.java index ed86a85de..c86734cc2 100644 --- a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/mediaimpl/JMFInit.java +++ b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/mediaimpl/JMFInit.java @@ -43,6 +43,7 @@ public class JMFInit extends Frame implements Runnable { private boolean visible = false; + @SuppressWarnings({"this-escape", "DoNotCall"}) public JMFInit(String[] args, boolean visible) { super("Initializing JMF..."); diff --git a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/mediaimpl/jmf/AudioChannel.java b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/mediaimpl/jmf/AudioChannel.java index 88cb3a649..61340f80e 100644 --- a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/mediaimpl/jmf/AudioChannel.java +++ b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/mediaimpl/jmf/AudioChannel.java @@ -322,7 +322,7 @@ public class AudioChannel { String encoding = codecFormat.getEncoding(); if (encoding.equalsIgnoreCase(AudioFormat.GSM) || encoding.equalsIgnoreCase(AudioFormat.GSM_RTP)) { - return milliseconds * 4; // 1 byte per millisec + return milliseconds * 4; // 1 byte per millisecond } else if (encoding.equalsIgnoreCase(AudioFormat.ULAW) || encoding.equalsIgnoreCase(AudioFormat.ULAW_RTP)) { diff --git a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/mediaimpl/jmf/AudioMediaSession.java b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/mediaimpl/jmf/AudioMediaSession.java index 42525c68d..22013df8b 100644 --- a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/mediaimpl/jmf/AudioMediaSession.java +++ b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/mediaimpl/jmf/AudioMediaSession.java @@ -53,6 +53,7 @@ public class AudioMediaSession extends JingleMediaSession { * @param locator media locator * @param jingleSession the jingle session. */ + @SuppressWarnings("this-escape") public AudioMediaSession(final PayloadType payloadType, final TransportCandidate remote, final TransportCandidate local, String locator, JingleSession jingleSession) { super(payloadType, remote, local, locator == null ? "dsound://" : locator, jingleSession); diff --git a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/mediaimpl/jspeex/AudioMediaSession.java b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/mediaimpl/jspeex/AudioMediaSession.java index 555836732..2874327ca 100644 --- a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/mediaimpl/jspeex/AudioMediaSession.java +++ b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/mediaimpl/jspeex/AudioMediaSession.java @@ -71,17 +71,17 @@ public class AudioMediaSession extends JingleMediaSession implements MediaSessio * @throws NoProcessorException if there is no media processor. * @throws UnsupportedFormatException if the format is not supported. * @throws IOException if an I/O error occurred. - * @throws GeneralSecurityException if there was a geneeral security exception. + * @throws GeneralSecurityException if there was a general security exception. */ public static MediaSession createSession(String localhost, int localPort, String remoteHost, int remotePort, MediaSessionListener eventHandler, int quality, boolean secure, boolean micOn) throws NoProcessorException, UnsupportedFormatException, IOException, GeneralSecurityException { SpeexFormat.setFramesPerPacket(1); - /** + /* * The master key. Hardcoded for now. */ byte[] masterKey = new byte[] {(byte) 0xE1, (byte) 0xF9, 0x7A, 0x0D, 0x3E, 0x01, (byte) 0x8B, (byte) 0xE0, (byte) 0xD6, 0x4F, (byte) 0xA3, 0x2C, 0x06, (byte) 0xDE, 0x41, 0x39}; - /** + /* * The master salt. Hardcoded for now. */ byte[] masterSalt = new byte[] {0x0E, (byte) 0xC6, 0x75, (byte) 0xAD, 0x49, (byte) 0x8A, (byte) 0xFE, (byte) 0xEB, (byte) 0xB6, (byte) 0x96, 0x0B, 0x3A, (byte) 0xAB, (byte) 0xE6}; @@ -104,6 +104,7 @@ public class AudioMediaSession extends JingleMediaSession implements MediaSessio * @param locator media locator * @param jingleSession the jingle session. */ + @SuppressWarnings("this-escape") public AudioMediaSession(final PayloadType payloadType, final TransportCandidate remote, final TransportCandidate local, String locator, JingleSession jingleSession) { super(payloadType, remote, local, locator == null ? "dsound://" : locator, jingleSession); diff --git a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/mediaimpl/sshare/ScreenShareSession.java b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/mediaimpl/sshare/ScreenShareSession.java index 8a33211e0..2cae0fc22 100644 --- a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/mediaimpl/sshare/ScreenShareSession.java +++ b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/mediaimpl/sshare/ScreenShareSession.java @@ -66,6 +66,7 @@ public class ScreenShareSession extends JingleMediaSession { * @param locator media locator * @param jingleSession the jingle session. */ + @SuppressWarnings("this-escape") public ScreenShareSession(final PayloadType payloadType, final TransportCandidate remote, final TransportCandidate local, final String locator, JingleSession jingleSession) { super(payloadType, remote, local, "Screen", jingleSession); diff --git a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/mediaimpl/sshare/api/ImageReceiver.java b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/mediaimpl/sshare/api/ImageReceiver.java index c3642d6dd..ed3010be7 100644 --- a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/mediaimpl/sshare/api/ImageReceiver.java +++ b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/mediaimpl/sshare/api/ImageReceiver.java @@ -48,6 +48,7 @@ public class ImageReceiver extends Canvas { private int remotePort; private ImageDecoder decoder; + @SuppressWarnings("this-escape") public ImageReceiver(final InetAddress remoteHost, final int remotePort, final int localPort, int width, int height) { tiles = new BufferedImage[width][height]; diff --git a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/mediaimpl/sshare/api/OctTreeQuantizer.java b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/mediaimpl/sshare/api/OctTreeQuantizer.java index bb6e7ff27..cff438024 100644 --- a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/mediaimpl/sshare/api/OctTreeQuantizer.java +++ b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/mediaimpl/sshare/api/OctTreeQuantizer.java @@ -76,6 +76,7 @@ public class OctTreeQuantizer implements Quantizer { private int colors = 0; private final List> colorList; + @SuppressWarnings({"JdkObsolete", "this-escape"}) public OctTreeQuantizer() { setup(256); colorList = new ArrayList<>(MAX_LEVEL + 1); diff --git a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/mediaimpl/test/TestMediaSession.java b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/mediaimpl/test/TestMediaSession.java index 1bb4cf746..edfa5a83a 100644 --- a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/mediaimpl/test/TestMediaSession.java +++ b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/mediaimpl/test/TestMediaSession.java @@ -37,6 +37,7 @@ public class TestMediaSession extends JingleMediaSession { * @param locator media locator * @param jingleSession the jingle session. */ + @SuppressWarnings("this-escape") public TestMediaSession(final PayloadType payloadType, final TransportCandidate remote, final TransportCandidate local, final String locator, JingleSession jingleSession) { super(payloadType, remote, local, "Test", jingleSession); diff --git a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/nat/FixedResolver.java b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/nat/FixedResolver.java index d1ea13fad..0a21ac4aa 100644 --- a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/nat/FixedResolver.java +++ b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/nat/FixedResolver.java @@ -38,6 +38,7 @@ public class FixedResolver extends TransportResolver { * @param ip the IP address. * @param port the port number. */ + @SuppressWarnings("this-escape") public FixedResolver(String ip, int port) { super(); setFixedCandidate(ip, port); diff --git a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/nat/ICEResolver.java b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/nat/ICEResolver.java index f286f5f38..95fce2b6c 100644 --- a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/nat/ICEResolver.java +++ b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/nat/ICEResolver.java @@ -56,6 +56,7 @@ public class ICEResolver extends TransportResolver { static Map negociatorsMap = new HashMap<>(); // ICENegociator iceNegociator = null; + @SuppressWarnings("this-escape") public ICEResolver(XMPPConnection connection, String server, int port) { super(); this.connection = connection; diff --git a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/nat/STUNResolver.java b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/nat/STUNResolver.java index b0b010f81..7eb49df5e 100644 --- a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/nat/STUNResolver.java +++ b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/nat/STUNResolver.java @@ -23,6 +23,7 @@ import java.net.SocketException; import java.net.URL; import java.util.ArrayList; import java.util.Enumeration; +import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; @@ -135,7 +136,7 @@ public class STUNResolver extends TransportResolver { * @param stunConfigStream An InputStream with the configuration file. * @return A list of loaded servers */ - public ArrayList loadSTUNServers(java.io.InputStream stunConfigStream) { + public List loadSTUNServers(java.io.InputStream stunConfigStream) { ArrayList serversList = new ArrayList<>(); String serverName; int serverPort; @@ -211,7 +212,7 @@ public class STUNResolver extends TransportResolver { * * @return a list of services */ - public ArrayList loadSTUNServers() { + public List loadSTUNServers() { ArrayList serversList = new ArrayList<>(); // Load the STUN configuration @@ -248,7 +249,7 @@ public class STUNResolver extends TransportResolver { * * @return the best STUN server that can be used. */ - private static STUNService bestSTUNServer(ArrayList listServers) { + private static STUNService bestSTUNServer(List listServers) { if (listServers.isEmpty()) { return null; } else { diff --git a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/nat/TcpUdpBridgeClient.java b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/nat/TcpUdpBridgeClient.java index 8e3936719..3451b7c1c 100644 --- a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/nat/TcpUdpBridgeClient.java +++ b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/nat/TcpUdpBridgeClient.java @@ -45,6 +45,7 @@ public class TcpUdpBridgeClient { private DatagramSocket localUdpSocket; private Socket localTcpSocket; + @SuppressWarnings("this-escape") public TcpUdpBridgeClient(String remoteTcpHost, String remoteUdpHost, int remoteTcpPort, int remoteUdpPort) { this.remoteTcpHost = remoteTcpHost; this.remoteUdpHost = remoteUdpHost; diff --git a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/nat/TcpUdpBridgeServer.java b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/nat/TcpUdpBridgeServer.java index 2ff8744b5..d5abc6bee 100644 --- a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/nat/TcpUdpBridgeServer.java +++ b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/nat/TcpUdpBridgeServer.java @@ -48,6 +48,7 @@ public class TcpUdpBridgeServer { private Socket localTcpSocket; private ServerSocket serverTcpSocket; + @SuppressWarnings("this-escape") public TcpUdpBridgeServer(String remoteTcpHost, String remoteUdpHost, int remoteTcpPort, int remoteUdpPort) { this.remoteTcpHost = remoteTcpHost; this.remoteUdpHost = remoteUdpHost; diff --git a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/nat/TransportCandidate.java b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/nat/TransportCandidate.java index 103286862..a9f3f7f8a 100644 --- a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/nat/TransportCandidate.java +++ b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/nat/TransportCandidate.java @@ -17,13 +17,13 @@ package org.jivesoftware.smackx.jingleold.nat; import java.io.IOException; -import java.io.UnsupportedEncodingException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.SocketException; import java.net.UnknownHostException; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -661,21 +661,13 @@ public abstract class TransportCandidate { String local = id.substring(0, keySplitIndex) + ";" + localUser; String remote = id.substring(keySplitIndex) + ";" + remoteUser; - try { - if (session.getConnection().getUser().equals(session.getInitiator())) { - - this.send = local.getBytes("UTF-8"); - this.receive = remote.getBytes("UTF-8"); - } else { - this.receive = local.getBytes("UTF-8"); - this.send = remote.getBytes("UTF-8"); - } + if (session.getConnection().getUser().equals(session.getInitiator())) { + this.send = local.getBytes(StandardCharsets.UTF_8); + this.receive = remote.getBytes(StandardCharsets.UTF_8); + } else { + this.receive = local.getBytes(StandardCharsets.UTF_8); + this.send = remote.getBytes(StandardCharsets.UTF_8); } - catch (UnsupportedEncodingException e) { - LOGGER.log(Level.WARNING, "exception", e); - } - - } @SuppressWarnings("UnusedVariable") @@ -706,7 +698,7 @@ public abstract class TransportCandidate { long delay = 100 / replyTries; - String[] str = new String(packet.getData(), "UTF-8").split(";"); + String[] str = new String(packet.getData(), StandardCharsets.UTF_8).split(";"); String pass = str[0]; String[] address = str[1].split(":"); String ip = address[0]; @@ -714,13 +706,7 @@ public abstract class TransportCandidate { if (pass.equals(candidate.getPassword()) && !accept) { - byte[] cont = null; - try { - cont = (password + ";" + candidate.getIp() + ":" + candidate.getPort()).getBytes("UTF-8"); - } - catch (UnsupportedEncodingException e) { - LOGGER.log(Level.WARNING, "exception", e); - } + byte[] cont = (password + ";" + candidate.getIp() + ":" + candidate.getPort()).getBytes(StandardCharsets.UTF_8); packet.setData(cont); packet.setLength(cont.length); @@ -778,31 +764,24 @@ public abstract class TransportCandidate { DatagramListener listener = new DatagramListener() { @Override public boolean datagramReceived(DatagramPacket datagramPacket) { + LOGGER.fine("ECHO Received to: " + candidate.getIp() + ":" + candidate.getPort() + " data: " + new String(datagramPacket.getData(), StandardCharsets.UTF_8)); + String[] str = new String(datagramPacket.getData(), StandardCharsets.UTF_8).split(";"); + String pass = str[0]; + String[] addr = str[1].split(":"); + String ip = addr[0]; + String pt = addr[1]; - try { - LOGGER.fine("ECHO Received to: " + candidate.getIp() + ":" + candidate.getPort() + " data: " + new String(datagramPacket.getData(), "UTF-8")); - String[] str = new String(datagramPacket.getData(), "UTF-8").split(";"); - String pass = str[0]; - String[] addr = str[1].split(":"); - String ip = addr[0]; - String pt = addr[1]; - - // CHECKSTYLE:OFF - if (pass.equals(password) - && transportCandidate.getIp().indexOf(ip) != -1 - && transportCandidate.getPort() == Integer.parseInt(pt)) { - // CHECKSTYLE:ON - LOGGER.fine("ECHO OK: " + candidate.getIp() + ":" + candidate.getPort() + " <-> " + transportCandidate.getIp() + ":" + transportCandidate.getPort()); - TestResult testResult = new TestResult(); - testResult.setResult(true); - ended = true; - fireTestResult(testResult, transportCandidate); - return true; - } - - } - catch (UnsupportedEncodingException e) { - LOGGER.log(Level.WARNING, "exception", e); + // CHECKSTYLE:OFF + if (pass.equals(password) + && transportCandidate.getIp().indexOf(ip) != -1 + && transportCandidate.getPort() == Integer.parseInt(pt)) { + // CHECKSTYLE:ON + LOGGER.fine("ECHO OK: " + candidate.getIp() + ":" + candidate.getPort() + " <-> " + transportCandidate.getIp() + ":" + transportCandidate.getPort()); + TestResult testResult = new TestResult(); + testResult.setResult(true); + ended = true; + fireTestResult(testResult, transportCandidate); + return true; } LOGGER.fine("ECHO Wrong Data: " + datagramPacket.getAddress().getHostAddress() + ":" + datagramPacket.getPort()); @@ -812,13 +791,7 @@ public abstract class TransportCandidate { addListener(listener); - byte[] content = null; - try { - content = new String(password + ";" + getIp() + ":" + getPort()).getBytes("UTF-8"); - } - catch (UnsupportedEncodingException e) { - LOGGER.log(Level.WARNING, "exception", e); - } + byte[] content = new String(password + ";" + getIp() + ":" + getPort()).getBytes(StandardCharsets.UTF_8); DatagramPacket packet = new DatagramPacket(content, content.length); diff --git a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/nat/TransportNegotiator.java b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/nat/TransportNegotiator.java index 127a7364c..c61ffd46f 100644 --- a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/nat/TransportNegotiator.java +++ b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/nat/TransportNegotiator.java @@ -95,7 +95,7 @@ public abstract class TransportNegotiator extends JingleNegotiator { * * @param session The Jingle session * @param transResolver The JingleTransportManager to use - * @param parentNegotiator the parent ngeotiator. + * @param parentNegotiator the parent negotiator. */ public TransportNegotiator(JingleSession session, TransportResolver transResolver, ContentNegotiator parentNegotiator) { super(session); @@ -311,7 +311,7 @@ public abstract class TransportNegotiator extends JingleNegotiator { // Sleep for some time, waiting for the candidates checks int totalTime = CANDIDATES_ACCEPT_PERIOD + TransportResolver.CHECK_TIMEOUT; - int tries = (int) Math.ceil(totalTime / 1000); + int tries = (int) Math.ceil(totalTime / 1000.0); for (int i = 0; i < tries - 1; i++) { try { @@ -478,7 +478,7 @@ public abstract class TransportNegotiator extends JingleNegotiator { * * @return The list of valid (ie, already checked) remote candidates. */ - final ArrayList getValidRemoteCandidatesList() { + final List getValidRemoteCandidatesList() { synchronized (validRemoteCandidates) { return new ArrayList<>(validRemoteCandidates); } @@ -872,7 +872,7 @@ public abstract class TransportNegotiator extends JingleNegotiator { @Override public TransportCandidate getBestRemoteCandidate() { // Hopefully, we only have one validRemoteCandidate - ArrayList cands = getValidRemoteCandidatesList(); + List cands = getValidRemoteCandidatesList(); if (!cands.isEmpty()) { LOGGER.fine("RAW CAND"); return cands.get(0); @@ -930,7 +930,7 @@ public abstract class TransportNegotiator extends JingleNegotiator { public TransportCandidate getBestRemoteCandidate() { ICECandidate result = null; - ArrayList cands = getValidRemoteCandidatesList(); + List cands = getValidRemoteCandidatesList(); if (!cands.isEmpty()) { int highest = -1; ICECandidate chose = null; diff --git a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/nat/TransportResolver.java b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/nat/TransportResolver.java index 75c1e2f71..9393dd1c3 100644 --- a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/nat/TransportResolver.java +++ b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/nat/TransportResolver.java @@ -210,7 +210,7 @@ public abstract class TransportResolver { * * @return the list of listeners */ - public ArrayList getListenersList() { + public List getListenersList() { synchronized (listeners) { return new ArrayList<>(listeners); } @@ -236,7 +236,7 @@ public abstract class TransportResolver { } /** - * Trigger a event notifying the initialization of the resolution process. + * Trigger an event notifying the initialization of the resolution process. */ private void triggerResolveInit() { Iterator iter = getListenersList().iterator(); @@ -250,7 +250,7 @@ public abstract class TransportResolver { } /** - * Trigger a event notifying the obtainment of all the candidates. + * Trigger an event notifying the obtainment of all the candidates. */ private void triggerResolveEnd() { Iterator iter = getListenersList().iterator(); diff --git a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/packet/Jingle.java b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/packet/Jingle.java index d904f0d34..9ac5270f6 100644 --- a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/packet/Jingle.java +++ b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/packet/Jingle.java @@ -71,6 +71,7 @@ public class Jingle extends IQ { * @param mi the jingle content info * @param sid the sid. */ + @SuppressWarnings("this-escape") public Jingle(final List contents, final JingleContentInfo mi, final String sid) { this(); @@ -93,6 +94,7 @@ public class Jingle extends IQ { * * @param content a content */ + @SuppressWarnings("this-escape") public Jingle(final JingleContent content) { this(); @@ -112,6 +114,7 @@ public class Jingle extends IQ { * * @param info The content info */ + @SuppressWarnings("this-escape") public Jingle(final JingleContentInfo info) { this(); @@ -131,6 +134,7 @@ public class Jingle extends IQ { * * @param action The action. */ + @SuppressWarnings("this-escape") public Jingle(final JingleActionEnum action) { this(null, null, null); this.action = action; diff --git a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/packet/JingleContentDescription.java b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/packet/JingleContentDescription.java index 3df98e574..53eee1d8a 100644 --- a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/packet/JingleContentDescription.java +++ b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/packet/JingleContentDescription.java @@ -105,7 +105,7 @@ public abstract class JingleContentDescription implements ExtensionElement { * * @return a list for the audio payloads in the packet. */ - public ArrayList getJinglePayloadTypesList() { + public List getJinglePayloadTypesList() { synchronized (payloads) { return new ArrayList<>(payloads); } @@ -116,7 +116,7 @@ public abstract class JingleContentDescription implements ExtensionElement { * * @return a list of PayloadType.Audio */ - public ArrayList getAudioPayloadTypesList() { + public List getAudioPayloadTypesList() { ArrayList result = new ArrayList<>(); Iterator jinglePtsIter = getJinglePayloadTypes(); @@ -184,6 +184,7 @@ public abstract class JingleContentDescription implements ExtensionElement { * * @param pt the payload type. */ + @SuppressWarnings("this-escape") public Audio(final JinglePayloadType pt) { super(); addJinglePayloadType(pt); diff --git a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/packet/JingleContentInfo.java b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/packet/JingleContentInfo.java index 9d5d7795b..caed15f9d 100644 --- a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/packet/JingleContentInfo.java +++ b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/packet/JingleContentInfo.java @@ -99,6 +99,7 @@ public class JingleContentInfo implements ExtensionElement { public static final String NAMESPACE = "urn:xmpp:tmp:jingle:apps:rtp"; + @SuppressWarnings("this-escape") public Audio(final ContentInfo mi) { super(mi); setNamespace(NAMESPACE); diff --git a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/packet/JingleDescription.java b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/packet/JingleDescription.java index 4e143f266..f2d2fdf82 100644 --- a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/packet/JingleDescription.java +++ b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/packet/JingleDescription.java @@ -191,6 +191,7 @@ public abstract class JingleDescription implements ExtensionElement { * * @param pt the payload type. */ + @SuppressWarnings("this-escape") public Audio(final PayloadType pt) { super(); addPayloadType(pt); diff --git a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/packet/JingleTransport.java b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/packet/JingleTransport.java index 40553ea27..1162df9d9 100644 --- a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/packet/JingleTransport.java +++ b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/packet/JingleTransport.java @@ -55,6 +55,7 @@ public class JingleTransport implements ExtensionElement { * * @param candidate A transport candidate element to add. */ + @SuppressWarnings("this-escape") public JingleTransport(final JingleTransportCandidate candidate) { super(); addCandidate(candidate); @@ -202,6 +203,7 @@ public class JingleTransport implements ExtensionElement { * * @param candidate the jmf transport candidate */ + @SuppressWarnings("this-escape") public JingleTransportCandidate(final TransportCandidate candidate) { super(); setMediaTransport(candidate); @@ -272,6 +274,7 @@ public class JingleTransport implements ExtensionElement { public static class Ice extends JingleTransport { public static final String NAMESPACE = "urn:xmpp:tmp:jingle:transports:ice-udp"; + @SuppressWarnings("this-escape") public Ice() { super(); setNamespace(NAMESPACE); @@ -359,6 +362,7 @@ public class JingleTransport implements ExtensionElement { public static class RawUdp extends JingleTransport { public static final String NAMESPACE = "http://www.xmpp.org/extensions/xep-0177.html#ns"; + @SuppressWarnings("this-escape") public RawUdp() { super(); setNamespace(NAMESPACE); diff --git a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/provider/JingleTransportProvider.java b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/provider/JingleTransportProvider.java index 0edb7d887..b81e4f201 100644 --- a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/provider/JingleTransportProvider.java +++ b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/provider/JingleTransportProvider.java @@ -88,7 +88,7 @@ public abstract class JingleTransportProvider extends ExtensionElementProvider listeners = new ArrayList<>(); private final Map> presenceMap = new HashMap<>(); // The roster is marked as initialized when at least a single roster packet - // has been recieved and processed. + // has been received and processed. boolean rosterInitialized = false; /** @@ -181,7 +181,7 @@ public class AgentRoster { /** * Returns true if the specified XMPP address is an agent in the workgroup. * - * @param jid the XMPP address of the agent (eg "jsmith@example.com"). The + * @param jid the XMPP address of the agent (e.g."jsmith@example.com"). The * address can be in any valid format (e.g. "domain/resource", "user@domain" * or "user@domain/resource"). * @return true if the XMPP address is an agent in the workgroup. @@ -203,12 +203,12 @@ public class AgentRoster { /** * Returns the presence info for a particular agent, or null if the agent - * is unavailable (offline) or if no presence information is available.

    + * is unavailable (offline) or if no presence information is available. * * @param user a fully qualified xmpp JID. The address could be in any valid format (e.g. * "domain/resource", "user@domain" or "user@domain/resource"). * @return the agent's current presence, or null if the agent is unavailable - * or if no presence information is available.. + * or if no presence information is available. */ public Presence getPresence(Jid user) { Jid key = getPresenceMapKey(user); diff --git a/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/agent/AgentSession.java b/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/agent/AgentSession.java index 4c2e57c52..0cdf52f9d 100644 --- a/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/agent/AgentSession.java +++ b/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/agent/AgentSession.java @@ -349,15 +349,9 @@ public class AgentSession { new StanzaTypeFilter(Presence.class), FromMatchesFilter.create(workgroupJID)), presence); presence = collector.nextResultOrThrow(); - - // We can safely update this iv since we didn't get any error - this.online = online; } // Otherwise the user is going offline... else { - // Update this iv now since we don't care at this point of any error - this.online = online; - presence = connection.getStanzaFactory().buildPresenceStanza() .ofType(Presence.Type.unavailable) .to(workgroupJID) @@ -710,10 +704,11 @@ public class AgentSession { } } + @SuppressWarnings("JavaUtilDate") private void fireOfferRequestEvent(OfferRequestProvider.OfferRequestPacket requestPacket) { Offer offer = new Offer(this.connection, this, requestPacket.getUserID(), requestPacket.getUserJID(), this.getWorkgroupJID(), - new Date(new Date().getTime() + (requestPacket.getTimeout() * 1000)), + new Date(new Date().getTime() + (requestPacket.getTimeout() * 1000L)), requestPacket.getSessionID(), requestPacket.getMetaData(), requestPacket.getContent()); synchronized (offerListeners) { @@ -723,6 +718,7 @@ public class AgentSession { } } + @SuppressWarnings("JavaUtilDate") private void fireOfferRevokeEvent(OfferRevokeProvider.OfferRevokePacket orp) { RevokedOffer revokedOffer = new RevokedOffer(orp.getUserJID(), orp.getUserID(), this.getWorkgroupJID(), orp.getSessionID(), orp.getReason(), new Date()); diff --git a/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/agent/Offer.java b/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/agent/Offer.java index ece31d9c6..3449cd69b 100644 --- a/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/agent/Offer.java +++ b/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/agent/Offer.java @@ -125,7 +125,7 @@ public class Offer { } /** - * The fully qualified name of the workgroup (eg support@example.com). + * The fully qualified name of the workgroup (e.g.support@example.com). * * @return the name of the workgroup. */ @@ -137,7 +137,7 @@ public class Offer { * The date when the offer will expire. The agent must {@link #accept()} * the offer before the expiration date or the offer will lapse and be * routed to another agent. Alternatively, the agent can {@link #reject()} - * the offer at any time if they don't wish to accept it.. + * the offer at any time if they don't wish to accept it. * * @return the date at which this offer expires. */ diff --git a/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/agent/OfferListener.java b/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/agent/OfferListener.java index 93de6bf8f..a33c6797b 100644 --- a/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/agent/OfferListener.java +++ b/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/agent/OfferListener.java @@ -37,7 +37,7 @@ public interface OfferListener { void offerReceived (Offer request); /** - * The implementing class instance will be notified via this when the AgentSessino has received + * The implementing class instance will be notified via this when the AgentSession has received * a revocation of a previously extended offer. * * @param revokedOffer the RevokedOffer instance embodying the details of the revoked offer diff --git a/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/ext/history/AgentChatHistory.java b/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/ext/history/AgentChatHistory.java index 87b03daf4..3fc5f4a1e 100644 --- a/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/ext/history/AgentChatHistory.java +++ b/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/ext/history/AgentChatHistory.java @@ -54,6 +54,7 @@ public class AgentChatHistory extends IQ { private final List agentChatSessions = new ArrayList<>(); + @SuppressWarnings("JavaUtilDate") public AgentChatHistory(EntityBareJid agentJID, int maxSessions, Date startDate) { this(); this.agentJID = agentJID; @@ -116,6 +117,7 @@ public class AgentChatHistory extends IQ { return agentChatHistory; } + @SuppressWarnings("JavaUtilDate") private static AgentChatSession parseChatSetting(XmlPullParser parser) throws XmlPullParserException, IOException { boolean done = false; diff --git a/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/packet/DepartQueuePacket.java b/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/packet/DepartQueuePacket.java index abe8771ca..8917ebe3e 100644 --- a/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/packet/DepartQueuePacket.java +++ b/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/packet/DepartQueuePacket.java @@ -53,6 +53,7 @@ public class DepartQueuePacket extends IQ { * @param workgroup the workgroup to depart. * @param user the user to make depart from the queue. */ + @SuppressWarnings("this-escape") public DepartQueuePacket(EntityBareJid workgroup, EntityJid user) { super("depart-queue", "http://jabber.org/protocol/workgroup"); this.user = user; diff --git a/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/packet/QueueDetails.java b/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/packet/QueueDetails.java index 19d7a0e3b..f29eb7738 100644 --- a/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/packet/QueueDetails.java +++ b/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/packet/QueueDetails.java @@ -145,6 +145,7 @@ public final class QueueDetails implements ExtensionElement { */ public static class Provider extends ExtensionElementProvider { + @SuppressWarnings("JavaUtilDate") @Override public QueueDetails parse(XmlPullParser parser, int initialDepth, XmlEnvironment xmlEnvironment) throws XmlPullParserException, diff --git a/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/settings/ChatSetting.java b/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/settings/ChatSetting.java index 00a227c61..499d3fac5 100644 --- a/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/settings/ChatSetting.java +++ b/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/settings/ChatSetting.java @@ -22,6 +22,7 @@ public class ChatSetting { private String value; private int type; + @SuppressWarnings("this-escape") public ChatSetting(String key, String value, int type) { setKey(key); setValue(value); diff --git a/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/settings/ChatSettings.java b/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/settings/ChatSettings.java index ddec1a662..8613deb96 100644 --- a/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/settings/ChatSettings.java +++ b/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/settings/ChatSettings.java @@ -66,6 +66,7 @@ public class ChatSettings extends IQ { settings = new ArrayList<>(); } + @SuppressWarnings("this-escape") public ChatSettings(String key) { this(); setKey(key); diff --git a/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/user/Workgroup.java b/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/user/Workgroup.java index 59ad7c4dc..a9f7b400c 100644 --- a/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/user/Workgroup.java +++ b/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/user/Workgroup.java @@ -91,7 +91,7 @@ public class Workgroup { /** * Creates a new workgroup instance using the specified workgroup JID - * (eg support@workgroup.example.com) and XMPP connection. The connection must have + * (e.g.support@workgroup.example.com) and XMPP connection. The connection must have * undergone a successful login before being used to construct an instance of * this class. * @@ -99,6 +99,7 @@ public class Workgroup { * @param connection an XMPP connection which must have already undergone a * successful login. */ + @SuppressWarnings("this-escape") public Workgroup(EntityBareJid workgroupJID, XMPPConnection connection) { // Login must have been done before passing in connection. if (!connection.isAuthenticated()) { @@ -136,8 +137,8 @@ public class Workgroup { } }); - /** - * Internal handling of an invitation.Recieving an invitation removes the user from the queue. + /* + * Internal handling of an invitation. Receiving an invitation removes the user from the queue. */ MultiUserChatManager.getInstanceFor(connection).addInvitationListener( new org.jivesoftware.smackx.muc.InvitationListener() { @@ -162,7 +163,7 @@ public class Workgroup { } /** - * Returns the name of this workgroup (eg support@example.com). + * Returns the name of this workgroup (e.g.support@example.com). * * @return the name of the workgroup. */ @@ -735,9 +736,9 @@ public class Workgroup { } /** - * Asks the workgroup for it's Properties. + * Asks the workgroup for its Properties. * - * @param jid the jid of the user who's information you would like the workgroup to retreive. + * @param jid the jid of the user whose information you would like the workgroup to retrieve. * @return the WorkgroupProperties for the specified workgroup. * @throws XMPPErrorException if there was an XMPP error returned. * @throws NoResponseException if there was no response from the remote entity. diff --git a/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/util/MetaDataUtils.java b/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/util/MetaDataUtils.java index 29170b479..24370229f 100644 --- a/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/util/MetaDataUtils.java +++ b/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/util/MetaDataUtils.java @@ -19,7 +19,7 @@ package org.jivesoftware.smackx.workgroup.util; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; -import java.util.Hashtable; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -45,6 +45,7 @@ public class MetaDataUtils { * @throws XmlPullParserException if an error occurs while parsing the XML. * @throws IOException if an error occurs while parsing the XML. */ + @SuppressWarnings("MixedMutabilityReturnType") public static Map> parseMetaData(XmlPullParser parser) throws XmlPullParserException, IOException { XmlPullParser.Event eventType = parser.getEventType(); @@ -52,7 +53,7 @@ public class MetaDataUtils { if ((eventType == XmlPullParser.Event.START_ELEMENT) && parser.getName().equals(MetaData.ELEMENT_NAME) && parser.getNamespace().equals(MetaData.NAMESPACE)) { - Map> metaData = new Hashtable<>(); + Map> metaData = new LinkedHashMap<>(); eventType = parser.next(); diff --git a/smack-legacy/src/main/java/org/jivesoftware/smackx/xroster/RosterExchangeManager.java b/smack-legacy/src/main/java/org/jivesoftware/smackx/xroster/RosterExchangeManager.java index baac6db8c..25461a3cd 100644 --- a/smack-legacy/src/main/java/org/jivesoftware/smackx/xroster/RosterExchangeManager.java +++ b/smack-legacy/src/main/java/org/jivesoftware/smackx/xroster/RosterExchangeManager.java @@ -42,7 +42,7 @@ import org.jxmpp.jid.Jid; /** * - * Manages Roster exchanges. A RosterExchangeManager provides a high level access to send + * Manages Roster exchanges. A RosterExchangeManager provides high level access to send * rosters, roster groups and roster entries to XMPP clients. It also provides an easy way * to hook up custom logic when entries are received from another XMPP client through * RosterExchangeListeners. @@ -106,7 +106,7 @@ public class RosterExchangeManager { * Removes a listener from roster exchanges. The listener will be fired anytime roster * entries are received from remote XMPP clients. * - * @param rosterExchangeListener a roster exchange listener.. + * @param rosterExchangeListener a roster exchange listener. */ public void removeRosterListener(RosterExchangeListener rosterExchangeListener) { rosterExchangeListeners.remove(rosterExchangeListener); diff --git a/smack-legacy/src/main/java/org/jivesoftware/smackx/xroster/packet/RosterExchange.java b/smack-legacy/src/main/java/org/jivesoftware/smackx/xroster/packet/RosterExchange.java index 291393660..6ee6771f1 100644 --- a/smack-legacy/src/main/java/org/jivesoftware/smackx/xroster/packet/RosterExchange.java +++ b/smack-legacy/src/main/java/org/jivesoftware/smackx/xroster/packet/RosterExchange.java @@ -70,6 +70,7 @@ public class RosterExchange implements ExtensionElement { * * @param roster the roster to send to other XMPP entity. */ + @SuppressWarnings("this-escape") public RosterExchange(Roster roster) { // Add all the roster entries to the new RosterExchange for (RosterEntry rosterEntry : roster.getEntries()) { diff --git a/smack-omemo-signal-integration-test/build.gradle b/smack-omemo-signal-integration-test/build.gradle index a499edc0c..58414a4d6 100644 --- a/smack-omemo-signal-integration-test/build.gradle +++ b/smack-omemo-signal-integration-test/build.gradle @@ -1,17 +1,16 @@ -apply plugin: 'application' +plugins { + id 'org.igniterealtime.smack.java-common-conventions' + id 'org.igniterealtime.smack.application-conventions' +} description = """\ Smack integration tests for OMEMO using libsignal.""" -mainClassName = 'org.igniterealtime.smack.inttest.smack_omemo_signal.SmackOmemoSignalIntegrationTestFramework' -applicationDefaultJvmArgs = ["-enableassertions"] +application { + mainClass = 'org.igniterealtime.smack.inttest.smack_omemo_signal.SmackOmemoSignalIntegrationTestFramework' +} dependencies { api project(':smack-integration-test') api project(':smack-omemo-signal') } - -run { - // Pass all system properties down to the "application" run - systemProperties System.getProperties() -} 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..dd5fd4602 100644 --- a/smack-omemo-signal/build.gradle +++ b/smack-omemo-signal/build.gradle @@ -1,3 +1,8 @@ +plugins { + id 'org.igniterealtime.smack.java-common-conventions' + id 'org.igniterealtime.smack.android-conventions' +} + description=""" Smack API for XEP-0384: OMEMO Encryption using libsignal """ @@ -6,7 +11,10 @@ dependencies { api project(":smack-im") api project(":smack-extensions") api project(":smack-omemo") - implementation 'org.whispersystems:signal-protocol-java:2.6.2' + implementation 'org.signal:libsignal-client:0.26.0' + + // TODO: Migrate Junit4 tests to Junit5. + testImplementation "org.junit.vintage:junit-vintage-engine:$junitVersion" 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/SignalCachingOmemoStore.java b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalCachingOmemoStore.java index 135bd96fb..eafd0fb2d 100644 --- a/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalCachingOmemoStore.java +++ b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalCachingOmemoStore.java @@ -23,15 +23,15 @@ package org.jivesoftware.smackx.omemo.signal; import org.jivesoftware.smackx.omemo.CachingOmemoStore; import org.jivesoftware.smackx.omemo.OmemoStore; -import org.whispersystems.libsignal.IdentityKey; -import org.whispersystems.libsignal.IdentityKeyPair; -import org.whispersystems.libsignal.SessionCipher; -import org.whispersystems.libsignal.SignalProtocolAddress; -import org.whispersystems.libsignal.ecc.ECPublicKey; -import org.whispersystems.libsignal.state.PreKeyBundle; -import org.whispersystems.libsignal.state.PreKeyRecord; -import org.whispersystems.libsignal.state.SessionRecord; -import org.whispersystems.libsignal.state.SignedPreKeyRecord; +import org.signal.libsignal.protocol.IdentityKey; +import org.signal.libsignal.protocol.IdentityKeyPair; +import org.signal.libsignal.protocol.SessionCipher; +import org.signal.libsignal.protocol.SignalProtocolAddress; +import org.signal.libsignal.protocol.ecc.ECPublicKey; +import org.signal.libsignal.protocol.state.PreKeyBundle; +import org.signal.libsignal.protocol.state.PreKeyRecord; +import org.signal.libsignal.protocol.state.SessionRecord; +import org.signal.libsignal.protocol.state.SignedPreKeyRecord; /** * Implementation of the CachingOmemoStore for smack-omemo-signal. diff --git a/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalFileBasedOmemoStore.java b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalFileBasedOmemoStore.java index f474b9b19..9cce37dfb 100644 --- a/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalFileBasedOmemoStore.java +++ b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalFileBasedOmemoStore.java @@ -25,15 +25,15 @@ import java.io.File; import org.jivesoftware.smackx.omemo.FileBasedOmemoStore; import org.jivesoftware.smackx.omemo.util.OmemoKeyUtil; -import org.whispersystems.libsignal.IdentityKey; -import org.whispersystems.libsignal.IdentityKeyPair; -import org.whispersystems.libsignal.SessionCipher; -import org.whispersystems.libsignal.SignalProtocolAddress; -import org.whispersystems.libsignal.ecc.ECPublicKey; -import org.whispersystems.libsignal.state.PreKeyBundle; -import org.whispersystems.libsignal.state.PreKeyRecord; -import org.whispersystems.libsignal.state.SessionRecord; -import org.whispersystems.libsignal.state.SignedPreKeyRecord; +import org.signal.libsignal.protocol.IdentityKey; +import org.signal.libsignal.protocol.IdentityKeyPair; +import org.signal.libsignal.protocol.SessionCipher; +import org.signal.libsignal.protocol.SignalProtocolAddress; +import org.signal.libsignal.protocol.ecc.ECPublicKey; +import org.signal.libsignal.protocol.state.PreKeyBundle; +import org.signal.libsignal.protocol.state.PreKeyRecord; +import org.signal.libsignal.protocol.state.SessionRecord; +import org.signal.libsignal.protocol.state.SignedPreKeyRecord; /** * Implementation of a FileBasedOmemoStore for the smack-omemo-signal module. diff --git a/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoKeyUtil.java b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoKeyUtil.java index 010eb65ed..fb748d5b4 100644 --- a/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoKeyUtil.java +++ b/smack-omemo-signal/src/main/java/org/jivesoftware/smackx/omemo/signal/SignalOmemoKeyUtil.java @@ -20,26 +20,28 @@ */ package org.jivesoftware.smackx.omemo.signal; -import java.io.IOException; -import java.util.List; -import java.util.TreeMap; - import org.jivesoftware.smackx.omemo.element.OmemoBundleElement; import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException; import org.jivesoftware.smackx.omemo.internal.OmemoDevice; import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint; import org.jivesoftware.smackx.omemo.util.OmemoKeyUtil; +import org.signal.libsignal.protocol.IdentityKey; +import org.signal.libsignal.protocol.IdentityKeyPair; +import org.signal.libsignal.protocol.InvalidKeyException; +import org.signal.libsignal.protocol.InvalidMessageException; +import org.signal.libsignal.protocol.ecc.Curve; +import org.signal.libsignal.protocol.ecc.ECKeyPair; +import org.signal.libsignal.protocol.ecc.ECPublicKey; +import org.signal.libsignal.protocol.state.PreKeyBundle; +import org.signal.libsignal.protocol.state.PreKeyRecord; +import org.signal.libsignal.protocol.state.SessionRecord; +import org.signal.libsignal.protocol.state.SignedPreKeyRecord; +import org.signal.libsignal.protocol.util.Medium; -import org.whispersystems.libsignal.IdentityKey; -import org.whispersystems.libsignal.IdentityKeyPair; -import org.whispersystems.libsignal.InvalidKeyException; -import org.whispersystems.libsignal.ecc.Curve; -import org.whispersystems.libsignal.ecc.ECPublicKey; -import org.whispersystems.libsignal.state.PreKeyBundle; -import org.whispersystems.libsignal.state.PreKeyRecord; -import org.whispersystems.libsignal.state.SessionRecord; -import org.whispersystems.libsignal.state.SignedPreKeyRecord; -import org.whispersystems.libsignal.util.KeyHelper; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.TreeMap; /** * Concrete implementation of the KeyUtil for an implementation using the Signal library. @@ -51,12 +53,13 @@ public class SignalOmemoKeyUtil extends OmemoKeyUtil generateOmemoPreKeys(int currentPreKeyId, int count) { - List preKeyRecords = KeyHelper.generatePreKeys(currentPreKeyId, count); + List preKeyRecords = generatePreKeys(currentPreKeyId, count); TreeMap map = new TreeMap<>(); for (PreKeyRecord p : preKeyRecords) { map.put(p.getId(), p); @@ -64,20 +67,44 @@ public class SignalOmemoKeyUtil extends OmemoKeyUtil generatePreKeys(int start, int count) { + List results = new ArrayList<>(count); + + start--; + + for (int i=0;i loadExistingSessions(List addresses) throws NoSessionException { + throw new RuntimeException("Not implemented."); + } + @Override public List getSubDeviceSessions(String s) { BareJid jid; diff --git a/smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/LegacySignalOmemoKeyUtilTest.java b/smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/LegacySignalOmemoKeyUtilTest.java index e7b91e0a8..db89d04c1 100644 --- a/smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/LegacySignalOmemoKeyUtilTest.java +++ b/smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/LegacySignalOmemoKeyUtilTest.java @@ -32,9 +32,9 @@ import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException; import org.jivesoftware.smackx.omemo.signal.SignalOmemoKeyUtil; import org.junit.Test; -import org.whispersystems.libsignal.IdentityKey; -import org.whispersystems.libsignal.IdentityKeyPair; -import org.whispersystems.libsignal.state.SignedPreKeyRecord; +import org.signal.libsignal.protocol.IdentityKey; +import org.signal.libsignal.protocol.IdentityKeyPair; +import org.signal.libsignal.protocol.state.SignedPreKeyRecord; /** * Test SignalOmemoKeyUtil methods. diff --git a/smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/SignalOmemoKeyUtilTest.java b/smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/SignalOmemoKeyUtilTest.java index d14e08ef5..4dd3e7f44 100644 --- a/smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/SignalOmemoKeyUtilTest.java +++ b/smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/SignalOmemoKeyUtilTest.java @@ -28,13 +28,13 @@ import org.jivesoftware.smackx.omemo.util.OmemoKeyUtil; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; -import org.whispersystems.libsignal.IdentityKey; -import org.whispersystems.libsignal.IdentityKeyPair; -import org.whispersystems.libsignal.ecc.ECPublicKey; -import org.whispersystems.libsignal.state.PreKeyBundle; -import org.whispersystems.libsignal.state.PreKeyRecord; -import org.whispersystems.libsignal.state.SessionRecord; -import org.whispersystems.libsignal.state.SignedPreKeyRecord; +import org.signal.libsignal.protocol.IdentityKey; +import org.signal.libsignal.protocol.IdentityKeyPair; +import org.signal.libsignal.protocol.ecc.ECPublicKey; +import org.signal.libsignal.protocol.state.PreKeyBundle; +import org.signal.libsignal.protocol.state.PreKeyRecord; +import org.signal.libsignal.protocol.state.SessionRecord; +import org.signal.libsignal.protocol.state.SignedPreKeyRecord; /** * smack-omemo-signal implementation of {@link OmemoKeyUtilTest}. diff --git a/smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/SignalOmemoStoreTest.java b/smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/SignalOmemoStoreTest.java index a4a9bedea..ce39a26ad 100644 --- a/smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/SignalOmemoStoreTest.java +++ b/smack-omemo-signal/src/test/java/org/jivesoftware/smackx/omemo/SignalOmemoStoreTest.java @@ -35,15 +35,15 @@ import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.jxmpp.stringprep.XmppStringprepException; -import org.whispersystems.libsignal.IdentityKey; -import org.whispersystems.libsignal.IdentityKeyPair; -import org.whispersystems.libsignal.SessionCipher; -import org.whispersystems.libsignal.SignalProtocolAddress; -import org.whispersystems.libsignal.ecc.ECPublicKey; -import org.whispersystems.libsignal.state.PreKeyBundle; -import org.whispersystems.libsignal.state.PreKeyRecord; -import org.whispersystems.libsignal.state.SessionRecord; -import org.whispersystems.libsignal.state.SignedPreKeyRecord; +import org.signal.libsignal.protocol.IdentityKey; +import org.signal.libsignal.protocol.IdentityKeyPair; +import org.signal.libsignal.protocol.SessionCipher; +import org.signal.libsignal.protocol.SignalProtocolAddress; +import org.signal.libsignal.protocol.ecc.ECPublicKey; +import org.signal.libsignal.protocol.state.PreKeyBundle; +import org.signal.libsignal.protocol.state.PreKeyRecord; +import org.signal.libsignal.protocol.state.SessionRecord; +import org.signal.libsignal.protocol.state.SignedPreKeyRecord; /** * smack-omemo-signal implementation of {@link OmemoStoreTest}. diff --git a/smack-omemo/build.gradle b/smack-omemo/build.gradle index bee2d806c..ca38595a0 100644 --- a/smack-omemo/build.gradle +++ b/smack-omemo/build.gradle @@ -1,3 +1,8 @@ +plugins { + id 'org.igniterealtime.smack.java-common-conventions' + id 'org.igniterealtime.smack.android-conventions' +} + description=""" Smack API for XEP-0384: OMEMO Encryption """ @@ -7,5 +12,8 @@ dependencies { api project(":smack-extensions") api project(":smack-experimental") + // TODO: Migrate Junit4 tests to Junit5. + testImplementation "org.junit.vintage:junit-vintage-engine:$junitVersion" + testFixturesApi(testFixtures(project(":smack-core"))) } diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/CachingOmemoStore.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/CachingOmemoStore.java index 766141b5c..f4c546bb3 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/CachingOmemoStore.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/CachingOmemoStore.java @@ -19,6 +19,7 @@ package org.jivesoftware.smackx.omemo; import java.io.IOException; import java.util.Date; import java.util.HashMap; +import java.util.Map; import java.util.SortedSet; import java.util.TreeMap; import java.util.TreeSet; @@ -268,8 +269,9 @@ public class CachingOmemoStore loadOmemoPreKeys(OmemoDevice userDevice) throws IOException { - TreeMap preKeys = getCache(userDevice).preKeys; + Map preKeys = getCache(userDevice).preKeys; if (preKeys.isEmpty() && persistent != null) { preKeys.putAll(persistent.loadOmemoPreKeys(userDevice)); @@ -293,8 +295,9 @@ public class CachingOmemoStore loadOmemoSignedPreKeys(OmemoDevice userDevice) throws IOException { - TreeMap sigPreKeys = getCache(userDevice).signedPreKeys; + Map sigPreKeys = getCache(userDevice).signedPreKeys; if (sigPreKeys.isEmpty() && persistent != null) { sigPreKeys.putAll(persistent.loadOmemoSignedPreKeys(userDevice)); @@ -341,7 +344,7 @@ public class CachingOmemoStore loadAllRawSessionsOf(OmemoDevice userDevice, BareJid contact) throws IOException { + public Map loadAllRawSessionsOf(OmemoDevice userDevice, BareJid contact) throws IOException { HashMap sessions = getCache(userDevice).sessions.get(contact); if (sessions == null) { sessions = new HashMap<>(); diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/FileBasedOmemoStore.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/FileBasedOmemoStore.java index 62b742db5..b05ffddb9 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/FileBasedOmemoStore.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/FileBasedOmemoStore.java @@ -125,12 +125,14 @@ public abstract class FileBasedOmemoStore loadOmemoPreKeys(OmemoDevice userDevice) throws IOException { File preKeyDirectory = hierarchy.getPreKeysDirectory(userDevice); TreeMap preKeys = new TreeMap<>(); @@ -234,6 +241,7 @@ public abstract class FileBasedOmemoStore loadOmemoSignedPreKeys(OmemoDevice userDevice) throws IOException { File signedPreKeysDirectory = hierarchy.getSignedPreKeysDirectory(userDevice); TreeMap signedPreKeys = new TreeMap<>(); @@ -290,6 +298,7 @@ public abstract class FileBasedOmemoStore loadAllRawSessionsOf(OmemoDevice userDevice, BareJid contact) throws IOException { File contactsDirectory = hierarchy.getContactsDir(userDevice, contact); HashMap sessions = new HashMap<>(); @@ -517,6 +526,7 @@ public abstract class FileBasedOmemoStore stack = new Stack<>(); diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoManager.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoManager.java index 92889f14e..5745204ba 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoManager.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoManager.java @@ -25,6 +25,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Random; import java.util.Set; import java.util.SortedSet; @@ -633,7 +634,7 @@ public final class OmemoManager extends Manager { * @throws SmackException.NoResponseException if there was no response from the remote entity. * @throws IOException if an I/O error occurred. */ - public synchronized HashMap getActiveFingerprints(BareJid contact) + public synchronized Map getActiveFingerprints(BareJid contact) throws SmackException.NotLoggedInException, CorruptedOmemoKeyException, CannotEstablishOmemoSessionException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, IOException { @@ -641,7 +642,7 @@ public final class OmemoManager extends Manager { throw new SmackException.NotLoggedInException(); } - HashMap fingerprints = new HashMap<>(); + Map fingerprints = new HashMap<>(); OmemoCachedDeviceList deviceList = getOmemoService().getOmemoStoreBackend().loadCachedDeviceList(getOwnDevice(), contact); diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoMessage.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoMessage.java index e2eb8635a..9f13b13c2 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoMessage.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoMessage.java @@ -22,6 +22,7 @@ import static org.jivesoftware.smackx.omemo.util.OmemoConstants.OMEMO_NAMESPACE_ import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.Set; import org.jivesoftware.smack.packet.Message; @@ -78,7 +79,7 @@ public class OmemoMessage { */ public static class Sent extends OmemoMessage { private final Set intendedDevices = new HashSet<>(); - private final HashMap skippedDevices = new HashMap<>(); + private final Map skippedDevices = new HashMap<>(); /** * Create a new outgoing OMEMO message. @@ -90,7 +91,7 @@ public class OmemoMessage { * @param skippedDevices devices which were skipped during encryption process because encryption * failed for some reason */ - Sent(OmemoElement element, byte[] key, byte[] iv, Set intendedDevices, HashMap skippedDevices) { + Sent(OmemoElement element, byte[] key, byte[] iv, Set intendedDevices, Map skippedDevices) { super(element, key, iv); this.intendedDevices.addAll(intendedDevices); this.skippedDevices.putAll(skippedDevices); @@ -110,7 +111,7 @@ public class OmemoMessage { * * @return map of skipped recipients and reasons for that. */ - public HashMap getSkippedDevices() { + public Map getSkippedDevices() { return skippedDevices; } diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoService.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoService.java index 82266bf2a..4573a44ab 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoService.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoService.java @@ -30,6 +30,7 @@ import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Random; import java.util.Set; import java.util.logging.Level; @@ -794,7 +795,7 @@ public abstract class OmemoService bundlesList = getOmemoStoreBackend().keyUtil().BUNDLE.bundles(bundleElement, contactsDevice); + Map bundlesList = getOmemoStoreBackend().keyUtil().BUNDLE.bundles(bundleElement, contactsDevice); int randomIndex = new Random().nextInt(bundlesList.size()); T_Bundle randomPreKeyBundle = new ArrayList<>(bundlesList.values()).get(randomIndex); @@ -893,7 +894,7 @@ public abstract class OmemoService signedPreKeys = loadOmemoSignedPreKeys(userDevice); + Map signedPreKeys = loadOmemoSignedPreKeys(userDevice); if (signedPreKeys.size() == 0) { changeSignedPreKey(userDevice); } @@ -237,7 +238,7 @@ public abstract class OmemoStore 0) { - TreeMap newKeys = generateOmemoPreKeys(startId + 1, newKeysCount); + Map newKeys = generateOmemoPreKeys(startId + 1, newKeysCount); storeOmemoPreKeys(userDevice, newKeys); } } @@ -415,6 +416,7 @@ public abstract class OmemoStore generateOmemoPreKeys(int startId, int count) { return keyUtil().generateOmemoPreKeys(startId, count); } @@ -449,7 +451,7 @@ public abstract class OmemoStore preKeyHashMap) throws IOException { + public void storeOmemoPreKeys(OmemoDevice userDevice, Map preKeyHashMap) throws IOException { for (Map.Entry entry : preKeyHashMap.entrySet()) { storeOmemoPreKey(userDevice, entry.getKey(), entry.getValue()); } @@ -472,6 +474,7 @@ public abstract class OmemoStore loadOmemoPreKeys(OmemoDevice userDevice) throws IOException; /** @@ -497,6 +500,8 @@ public abstract class OmemoStore loadOmemoSignedPreKeys(OmemoDevice userDevice) throws IOException; /** @@ -552,7 +557,7 @@ public abstract class OmemoStore loadAllRawSessionsOf(OmemoDevice userDevice, BareJid contact) throws IOException; + public abstract Map loadAllRawSessionsOf(OmemoDevice userDevice, BareJid contact) throws IOException; /** * Store a crypto-lib specific session to storage. diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoBundleElement.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoBundleElement.java index 217c27f6d..610e07515 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoBundleElement.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoBundleElement.java @@ -47,8 +47,8 @@ public abstract class OmemoBundleElement implements ExtensionElement { private byte[] signedPreKeySignature; private final String identityKeyB64; private byte[] identityKey; - private final HashMap preKeysB64; - private HashMap preKeys; + private final Map preKeysB64; + private Map preKeys; /** * Constructor to create a Bundle Element from base64 Strings. @@ -57,9 +57,9 @@ public abstract class OmemoBundleElement implements ExtensionElement { * @param signedPreKeyB64 base64 encoded signedPreKey * @param signedPreKeySigB64 base64 encoded signedPreKeySignature * @param identityKeyB64 base64 encoded identityKey - * @param preKeysB64 HashMap of base64 encoded preKeys + * @param preKeysB64 Map of base64 encoded preKeys */ - public OmemoBundleElement(int signedPreKeyId, String signedPreKeyB64, String signedPreKeySigB64, String identityKeyB64, HashMap preKeysB64) { + public OmemoBundleElement(int signedPreKeyId, String signedPreKeyB64, String signedPreKeySigB64, String identityKeyB64, Map preKeysB64) { if (signedPreKeyId < 0) { throw new IllegalArgumentException("signedPreKeyId MUST be greater than or equal to 0."); } @@ -81,9 +81,9 @@ public abstract class OmemoBundleElement implements ExtensionElement { * @param signedPreKey signedPreKey * @param signedPreKeySig signedPreKeySignature * @param identityKey identityKey - * @param preKeys HashMap of preKeys + * @param preKeys Map of preKeys */ - public OmemoBundleElement(int signedPreKeyId, byte[] signedPreKey, byte[] signedPreKeySig, byte[] identityKey, HashMap preKeys) { + public OmemoBundleElement(int signedPreKeyId, byte[] signedPreKey, byte[] signedPreKeySig, byte[] identityKey, Map preKeys) { this(signedPreKeyId, signedPreKey != null ? Base64.encodeToString(signedPreKey) : null, signedPreKeySig != null ? Base64.encodeToString(signedPreKeySig) : null, @@ -95,12 +95,12 @@ public abstract class OmemoBundleElement implements ExtensionElement { this.preKeys = preKeys; } - private static HashMap base64EncodePreKeys(HashMap preKeys) { + private static Map base64EncodePreKeys(Map preKeys) { if (preKeys == null) { return null; } - HashMap converted = new HashMap<>(); + Map converted = new HashMap<>(); for (Integer id : preKeys.keySet()) { converted.put(id, Base64.encodeToString(preKeys.get(id))); } @@ -155,12 +155,12 @@ public abstract class OmemoBundleElement implements ExtensionElement { } /** - * Return the HashMap of preKeys in the bundle. + * Return the Map of preKeys in the bundle. * The map uses the preKeys ids as key and the preKeys as value. * * @return preKeys Pre-Keys contained in the bundle */ - public HashMap getPreKeys() { + public Map getPreKeys() { if (preKeys == null) { preKeys = new HashMap<>(); for (int id : preKeysB64.keySet()) { diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoBundleElement_VAxolotl.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoBundleElement_VAxolotl.java index bcf616111..e3ae2d8aa 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoBundleElement_VAxolotl.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoBundleElement_VAxolotl.java @@ -18,7 +18,7 @@ package org.jivesoftware.smackx.omemo.element; import static org.jivesoftware.smackx.omemo.util.OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL; -import java.util.HashMap; +import java.util.Map; /** * OMEMO device bundle as described by the protocol. @@ -29,11 +29,11 @@ import java.util.HashMap; */ public class OmemoBundleElement_VAxolotl extends OmemoBundleElement { - public OmemoBundleElement_VAxolotl(int signedPreKeyId, String signedPreKeyB64, String signedPreKeySigB64, String identityKeyB64, HashMap preKeysB64) { + public OmemoBundleElement_VAxolotl(int signedPreKeyId, String signedPreKeyB64, String signedPreKeySigB64, String identityKeyB64, Map preKeysB64) { super(signedPreKeyId, signedPreKeyB64, signedPreKeySigB64, identityKeyB64, preKeysB64); } - public OmemoBundleElement_VAxolotl(int signedPreKeyId, byte[] signedPreKey, byte[] signedPreKeySig, byte[] identityKey, HashMap preKeys) { + public OmemoBundleElement_VAxolotl(int signedPreKeyId, byte[] signedPreKey, byte[] signedPreKeySig, byte[] identityKey, Map preKeys) { super(signedPreKeyId, signedPreKey, signedPreKeySig, identityKey, preKeys); } diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoHeaderElement.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoHeaderElement.java index 4270d8ec5..050adf033 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoHeaderElement.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/element/OmemoHeaderElement.java @@ -57,7 +57,7 @@ public abstract class OmemoHeaderElement implements XmlElement { return sid; } - public ArrayList getKeys() { + public List getKeys() { return new ArrayList<>(keys); } diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/CannotEstablishOmemoSessionException.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/CannotEstablishOmemoSessionException.java index 2d4e65513..161dbc38f 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/CannotEstablishOmemoSessionException.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/exceptions/CannotEstablishOmemoSessionException.java @@ -18,6 +18,7 @@ package org.jivesoftware.smackx.omemo.exceptions; import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.jivesoftware.smackx.omemo.internal.OmemoDevice; @@ -32,8 +33,8 @@ import org.jxmpp.jid.BareJid; public class CannotEstablishOmemoSessionException extends Exception { private static final long serialVersionUID = 3165844730283295249L; - private final HashMap> failures = new HashMap<>(); - private final HashMap> successes = new HashMap<>(); + private final Map> failures = new HashMap<>(); + private final Map> successes = new HashMap<>(); public CannotEstablishOmemoSessionException(OmemoDevice failed, Throwable reason) { super(); @@ -41,7 +42,7 @@ public class CannotEstablishOmemoSessionException extends Exception { } public void addFailures(CannotEstablishOmemoSessionException otherFailures) { - for (Map.Entry> entry : otherFailures.getFailures().entrySet()) { + for (Map.Entry> entry : otherFailures.getFailures().entrySet()) { getFailsOfContact(entry.getKey()).putAll(entry.getValue()); } } @@ -50,16 +51,16 @@ public class CannotEstablishOmemoSessionException extends Exception { getSuccessesOfContact(success.getJid()).add(success); } - public HashMap> getFailures() { + public Map> getFailures() { return failures; } - public HashMap> getSuccesses() { + public Map> getSuccesses() { return successes; } - private HashMap getFailsOfContact(BareJid contact) { - HashMap h = failures.get(contact); + private Map getFailsOfContact(BareJid contact) { + Map h = failures.get(contact); if (h == null) { h = new HashMap<>(); failures.put(contact, h); @@ -67,8 +68,8 @@ public class CannotEstablishOmemoSessionException extends Exception { return h; } - private ArrayList getSuccessesOfContact(BareJid contact) { - ArrayList suc = successes.get(contact); + private List getSuccessesOfContact(BareJid contact) { + List suc = successes.get(contact); if (suc == null) { suc = new ArrayList<>(); successes.put(contact, suc); @@ -83,8 +84,8 @@ public class CannotEstablishOmemoSessionException extends Exception { * @return true if the exception requires to be thrown */ public boolean requiresThrowing() { - for (Map.Entry> entry : failures.entrySet()) { - ArrayList suc = successes.get(entry.getKey()); + for (Map.Entry> entry : failures.entrySet()) { + List suc = successes.get(entry.getKey()); if (suc == null || suc.isEmpty()) { return true; } 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-omemo/src/main/java/org/jivesoftware/smackx/omemo/package-info.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/package-info.java index b8e0016ba..d4e6fbddd 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/package-info.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/package-info.java @@ -143,7 +143,7 @@ * *

    * The `setup()` method registers the service as a singleton. You can later access the instance by calling - * `SignalOmemoService.getInstace()`. The service can only be registered once. Subsequent calls will throw an + * `SignalOmemoService.getInstance()`. The service can only be registered once. Subsequent calls will throw an * {@link IllegalStateException}. *

    *

    2. Set an OmemoStore

    diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoKeyUtil.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoKeyUtil.java index 607181552..39f9ace13 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoKeyUtil.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoKeyUtil.java @@ -119,8 +119,8 @@ public abstract class OmemoKeyUtil bundles(OmemoBundleElement bundle, OmemoDevice contact) throws CorruptedOmemoKeyException { - HashMap bundles = new HashMap<>(); + public Map bundles(OmemoBundleElement bundle, OmemoDevice contact) throws CorruptedOmemoKeyException { + Map bundles = new HashMap<>(); for (int deviceId : bundle.getPreKeys().keySet()) { try { bundles.put(deviceId, bundleFromOmemoBundle(bundle, contact, deviceId)); @@ -211,6 +211,8 @@ public abstract class OmemoKeyUtil generateOmemoPreKeys(int startId, int count); /** @@ -338,8 +340,8 @@ public abstract class OmemoKeyUtil preKeyPublicKeysForBundle(TreeMap preKeyHashMap) { - HashMap out = new HashMap<>(); + public Map preKeyPublicKeysForBundle(Map preKeyHashMap) { + Map out = new HashMap<>(); for (Map.Entry e : preKeyHashMap.entrySet()) { out.put(e.getKey(), preKeyForBundle(e.getValue())); } diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoMessageBuilder.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoMessageBuilder.java index 0810fe1d1..5144b4f02 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoMessageBuilder.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoMessageBuilder.java @@ -83,7 +83,7 @@ public class OmemoMessageBuilder bundles = keyUtil.BUNDLE.bundles(bundle, device); + Map bundles = keyUtil.BUNDLE.bundles(bundle, device); assertEquals("There must be 100 bundles in the HashMap.", 100, bundles.size()); assertNotNull(keyUtil.BUNDLE.identityKey(bundle)); diff --git a/smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/OmemoServiceTest.java b/smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/OmemoServiceTest.java index 67dd244b6..0f228b161 100644 --- a/smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/OmemoServiceTest.java +++ b/smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/OmemoServiceTest.java @@ -50,6 +50,7 @@ public class OmemoServiceTest extends SmackTestSuite { * @throws XmppStringprepException if the provided string is invalid. */ @Test + @SuppressWarnings("JavaUtilDate") public void isStaleDeviceTest() throws XmppStringprepException { OmemoDevice user = new OmemoDevice(JidCreate.bareFrom("alice@wonderland.lit"), 123); OmemoDevice other = new OmemoDevice(JidCreate.bareFrom("bob@builder.tv"), 444); diff --git a/smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/OmemoStoreTest.java b/smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/OmemoStoreTest.java index 909aad68c..22b626054 100644 --- a/smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/OmemoStoreTest.java +++ b/smack-omemo/src/test/java/org/jivesoftware/smackx/omemo/OmemoStoreTest.java @@ -27,8 +27,8 @@ import static junit.framework.TestCase.assertTrue; import java.io.IOException; import java.util.Arrays; import java.util.Date; -import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.TreeMap; import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException; @@ -174,7 +174,7 @@ public abstract class OmemoStoreTest before = store.generateOmemoPreKeys(1, 10); + Map before = store.generateOmemoPreKeys(1, 10); assertEquals("The store must have no prekeys before this test.", 0, store.loadOmemoPreKeys(alice).size()); store.storeOmemoPreKeys(alice, before); @@ -247,8 +247,8 @@ public abstract class OmemoStoreTest sessions = store.loadAllRawSessionsOf(alice, bob.getJid()); + Map sessions = store.loadAllRawSessionsOf(alice, bob.getJid()); assertNotNull(sessions); assertEquals(0, sessions.size()); } diff --git a/smack-openpgp/build.gradle b/smack-openpgp/build.gradle index cde598465..1a1fcaafe 100644 --- a/smack-openpgp/build.gradle +++ b/smack-openpgp/build.gradle @@ -1,3 +1,8 @@ +plugins { + id 'org.igniterealtime.smack.java-common-conventions' + id 'org.igniterealtime.smack.android-conventions' +} + description = """\ Smack API for XEP-0373: OpenPGP for XMPP.""" @@ -8,10 +13,13 @@ 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}" testFixturesApi(testFixtures(project(":smack-core"))) testImplementation group: 'commons-io', name: 'commons-io', version: "$commonsIoVersion" + + // TODO: Migrate Junit4 tests to Junit5. + testImplementation "org.junit.vintage:junit-vintage-engine:$junitVersion" } diff --git a/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/OpenPgpContact.java b/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/OpenPgpContact.java index b7ffa0e71..c575fc5b7 100644 --- a/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/OpenPgpContact.java +++ b/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/OpenPgpContact.java @@ -362,6 +362,7 @@ public class OpenPgpContact { * @throws SmackException.NoResponseException in case the server doesn't respond. * @throws IOException IO is dangerous. */ + @SuppressWarnings("JavaUtilDate") public void updateKeys(XMPPConnection connection, PublicKeysListElement metadata) throws InterruptedException, SmackException.NotConnectedException, SmackException.NoResponseException, IOException { diff --git a/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/OpenPgpManager.java b/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/OpenPgpManager.java index 645f5bb66..b4a22c7cc 100644 --- a/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/OpenPgpManager.java +++ b/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/OpenPgpManager.java @@ -246,6 +246,7 @@ public final class OpenPgpManager extends Manager { * @throws SmackException.NotLoggedInException if we are not logged in. * @throws PGPException if something goes wrong during key loading/generating */ + @SuppressWarnings("JavaUtilDate") public void announceSupportAndPublish() throws NoSuchAlgorithmException, NoSuchProviderException, InterruptedException, PubSubException.NotALeafNodeException, XMPPException.XMPPErrorException, diff --git a/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/OpenPgpSelf.java b/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/OpenPgpSelf.java index 6ada87150..c9f6d6d17 100644 --- a/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/OpenPgpSelf.java +++ b/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/OpenPgpSelf.java @@ -71,6 +71,7 @@ public class OpenPgpSelf extends OpenPgpContact { * @throws IOException IO is dangerous * @throws PGPException PGP is brittle */ + @SuppressWarnings("JavaUtilDate") public PGPSecretKeyRing getSigningKeyRing() throws IOException, PGPException { PGPSecretKeyRingCollection secretKeyRings = getSecretKeys(); if (secretKeyRings == null) { 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-openpgp/src/main/java/org/jivesoftware/smackx/ox/element/EncryptedOpenPgpContentElement.java b/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/element/EncryptedOpenPgpContentElement.java index a79dc8997..ecf49010f 100644 --- a/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/element/EncryptedOpenPgpContentElement.java +++ b/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/element/EncryptedOpenPgpContentElement.java @@ -1,6 +1,6 @@ /** * - * Copyright 2017-2019 Florian Schmaus, 2018 Paul Schaub. + * Copyright 2017-2024 Florian Schmaus, 2018 Paul Schaub. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,6 +45,7 @@ public abstract class EncryptedOpenPgpContentElement extends OpenPgpContentEleme this.rpad = Objects.requireNonNull(rpad); } + @SuppressWarnings("JavaUtilDate") protected EncryptedOpenPgpContentElement(Set to, List payload) { super(Objects.requireNonNullNorEmpty( to, "Encrypted OpenPGP content elements must have at least one 'to' attribute."), diff --git a/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/element/OpenPgpContentElement.java b/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/element/OpenPgpContentElement.java index 974185573..07d9327cb 100644 --- a/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/element/OpenPgpContentElement.java +++ b/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/element/OpenPgpContentElement.java @@ -132,7 +132,7 @@ public abstract class OpenPgpContentElement implements ExtensionElement { * @param type of the ExtensionElement. * @return the extension, or null if it doesn't exist. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) public PE getExtension(String elementName, String namespace) { if (namespace == null) { return null; diff --git a/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/element/PublicKeysListElement.java b/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/element/PublicKeysListElement.java index d4b9e81c7..3f97d83e8 100644 --- a/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/element/PublicKeysListElement.java +++ b/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/element/PublicKeysListElement.java @@ -40,7 +40,7 @@ public final class PublicKeysListElement implements ExtensionElement { private final Map metadata; - private PublicKeysListElement(TreeMap metadata) { + private PublicKeysListElement(Map metadata) { this.metadata = Collections.unmodifiableMap(Objects.requireNonNull(metadata)); } @@ -48,7 +48,8 @@ public final class PublicKeysListElement implements ExtensionElement { return new Builder(); } - public TreeMap getMetadata() { + @SuppressWarnings("NonApiType") + public Map getMetadata() { return new TreeMap<>(metadata); } @@ -72,7 +73,7 @@ public final class PublicKeysListElement implements ExtensionElement { public static final class Builder { - private final TreeMap metadata = new TreeMap<>(); + private final Map metadata = new TreeMap<>(); private Builder() { // Empty @@ -133,12 +134,13 @@ public final class PublicKeysListElement implements ExtensionElement { return xml; } + @SuppressWarnings("JavaUtilDate") @Override public int hashCode() { return getV4Fingerprint().hashCode() + 3 * getDate().hashCode(); } - @SuppressWarnings("UndefinedEquals") + @SuppressWarnings({"UndefinedEquals", "JavaUtilDate"}) // TODO: Fix the UndefinedEquals due using Date.equals(Date) @Override public boolean equals(Object o) { diff --git a/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/provider/OpenPgpContentElementProvider.java b/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/provider/OpenPgpContentElementProvider.java index 9bba1ef5e..616ac8779 100644 --- a/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/provider/OpenPgpContentElementProvider.java +++ b/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/provider/OpenPgpContentElementProvider.java @@ -1,6 +1,6 @@ /** * - * Copyright 2017-2021 Florian Schmaus, 2018 Paul Schaub. + * Copyright 2017-2024 Florian Schmaus, 2018 Paul Schaub. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,9 +18,9 @@ package org.jivesoftware.smackx.ox.provider; import java.io.IOException; import java.text.ParseException; +import java.util.ArrayList; import java.util.Date; import java.util.HashSet; -import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.logging.Level; @@ -87,7 +87,7 @@ public abstract class OpenPgpContentElementProvider to = new HashSet<>(); Date timestamp = null; String rpad = null; - List payload = new LinkedList<>(); + List payload = new ArrayList<>(); outerloop: while (true) { XmlPullParser.Event tag = parser.next(); diff --git a/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/store/filebased/FileBasedOpenPgpMetadataStore.java b/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/store/filebased/FileBasedOpenPgpMetadataStore.java index 9d20e2cfa..d1f0b2d6b 100644 --- a/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/store/filebased/FileBasedOpenPgpMetadataStore.java +++ b/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/store/filebased/FileBasedOpenPgpMetadataStore.java @@ -118,6 +118,7 @@ public class FileBasedOpenPgpMetadataStore extends AbstractOpenPgpMetadataStore } } + @SuppressWarnings("JavaUtilDate") static void writeFingerprintsAndDates(Map data, File destination) throws IOException { if (data == null || data.isEmpty()) { diff --git a/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/util/OpenPgpPubSubUtil.java b/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/util/OpenPgpPubSubUtil.java index 0e418d80b..1ba31495d 100644 --- a/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/util/OpenPgpPubSubUtil.java +++ b/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/util/OpenPgpPubSubUtil.java @@ -124,6 +124,7 @@ public class OpenPgpPubSubUtil { * @throws SmackException.NotConnectedException if we are not connected. * @throws SmackException.NoResponseException if the server doesn't respond. */ + @SuppressWarnings("JavaUtilDate") public static void publishPublicKey(PepManager pepManager, PubkeyElement pubkeyElement, OpenPgpV4Fingerprint fingerprint) throws InterruptedException, PubSubException.NotALeafNodeException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException { @@ -408,7 +409,7 @@ public class OpenPgpPubSubUtil { * which are not subscribed to the node owner. Therefore this method fetches the node directly and puts it * into the {@link PubSubManager}s node map. * - * Note: Due to the alck of a disco#info query, it might happen, that the node doesn't exist on the server, + * Note: Due to the lack of a disco#info query, it might happen, that the node doesn't exist on the server, * even though we add it to the node map. * * @see Ejabberd bug tracker about the issue @@ -464,8 +465,7 @@ public class OpenPgpPubSubUtil { } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | InstantiationException | NoSuchFieldException e) { - LOGGER.log(Level.SEVERE, "Using reflections to create a LeafNode and put it into PubSubManagers nodeMap failed.", e); - throw new AssertionError(e); + throw new LinkageError("Using reflections to create a LeafNode and put it into PubSubManagers nodeMap failed.", e); } } } diff --git a/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox/OpenPgpElementTest.java b/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox/OpenPgpElementTest.java index 2c54a0d55..a958d1adb 100644 --- a/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox/OpenPgpElementTest.java +++ b/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox/OpenPgpElementTest.java @@ -51,6 +51,7 @@ public class OpenPgpElementTest extends SmackTestSuite { private final Set recipients; // 2014-07-10T15:06:00.000+00:00 + @SuppressWarnings("JavaUtilDate") private static final Date testDate = new Date(1405004760000L); public OpenPgpElementTest() throws XmppStringprepException { diff --git a/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox/OpenPgpStoreTest.java b/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox/OpenPgpStoreTest.java index d64c1522c..30e3f05af 100644 --- a/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox/OpenPgpStoreTest.java +++ b/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox/OpenPgpStoreTest.java @@ -300,6 +300,7 @@ public class OpenPgpStoreTest extends SmackTestSuite { */ @Test + @SuppressWarnings("JavaUtilDate") public void t10_meta_emptyStoreTest() throws IOException { assertNotNull(openPgpStoreInstance1.getAnnouncedFingerprintsOf(alice)); assertTrue(openPgpStoreInstance1.getAnnouncedFingerprintsOf(alice).isEmpty()); @@ -324,6 +325,7 @@ public class OpenPgpStoreTest extends SmackTestSuite { } @Test + @SuppressWarnings("JavaUtilDate") public void t11_key_fetchDateTest() throws IOException { Map fetchDates1 = openPgpStoreInstance1.getPublicKeyFetchDates(alice); diff --git a/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox/PainlessOpenPgpProviderTest.java b/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox/PainlessOpenPgpProviderTest.java index 5f1210367..41319bd6f 100644 --- a/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox/PainlessOpenPgpProviderTest.java +++ b/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox/PainlessOpenPgpProviderTest.java @@ -72,6 +72,7 @@ public class PainlessOpenPgpProviderTest extends SmackTestSuite { } @Test + @SuppressWarnings("JavaUtilDate") public void encryptDecryptTest() throws PGPException, NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException, IOException, MissingUserIdOnKeyException, XmlPullParserException { // Initialize diff --git a/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox/PublicKeysListElementTest.java b/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox/PublicKeysListElementTest.java index 319c60bcf..eb7f3922c 100644 --- a/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox/PublicKeysListElementTest.java +++ b/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox/PublicKeysListElementTest.java @@ -70,6 +70,7 @@ public class PublicKeysListElementTest extends SmackTestSuite { } @Test + @SuppressWarnings("JavaUtilDate") public void listBuilderRefusesDuplicatesTest() { PublicKeysListElement.Builder builder = PublicKeysListElement.builder(); String fp40 = "49545320414c4c2041424f555420444120484558"; diff --git a/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox_im/OXInstantMessagingManagerTest.java b/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox_im/OXInstantMessagingManagerTest.java index 7854aedce..668a14204 100644 --- a/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox_im/OXInstantMessagingManagerTest.java +++ b/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox_im/OXInstantMessagingManagerTest.java @@ -66,6 +66,7 @@ public class OXInstantMessagingManagerTest extends SmackTestSuite { } @Test + @SuppressWarnings("JavaUtilDate") public void test() throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchProviderException, SmackException, MissingUserIdOnKeyException, InterruptedException, XMPPException, XmlPullParserException { diff --git a/smack-repl/build.gradle b/smack-repl/build.gradle index 70e055ab7..9b60cd9fb 100644 --- a/smack-repl/build.gradle +++ b/smack-repl/build.gradle @@ -1,35 +1,32 @@ plugins { - id "com.github.alisiikh.scalastyle_2.12" version "2.0.2" + id 'org.igniterealtime.smack.java-common-conventions' + id "com.github.alisiikh.scalastyle" version "3.5.0" } description = """\ A REPL (Read-Eval-Print-Loop) for Smack, or, in other words, a CLI (Command Line Interface) for Smack.""" apply plugin: 'scala' -apply plugin: 'com.github.alisiikh.scalastyle_2.12' +apply plugin: 'com.github.alisiikh.scalastyle' ext { - scalaVersion = '2.13.6' + scalaVersion = '2.13.13' } 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:3.0.0-M1" } -scalaStyle { +scalastyle { config = new File(rootConfigDir, 'scalaStyle.xml') verbose = true - failOnViolation = true + failOnWarning = true } -check.dependsOn(scalaStyleCheck) +check.dependsOn(scalastyleCheck) task printClasspath(dependsOn: assemble) { doLast { diff --git a/smack-repl/scala.repl b/smack-repl/scala.repl index 1da355d19..0e0dee4db 100644 --- a/smack-repl/scala.repl +++ b/smack-repl/scala.repl @@ -5,4 +5,5 @@ import org.jivesoftware.smack.util.TLSUtils import org.jivesoftware.smack.tcp._ import org.jxmpp.jid.impl.JidCreate -import org.igniterealtime.smack.smackrepl.IoT._ +import org.igniterealtime.smack.examples._ +import org.igniterealtime.smack.examples.IoT._ diff --git a/smack-resolver-dnsjava/build.gradle b/smack-resolver-dnsjava/build.gradle index b17683ecc..b4e6b85cf 100644 --- a/smack-resolver-dnsjava/build.gradle +++ b/smack-resolver-dnsjava/build.gradle @@ -1,3 +1,8 @@ +plugins { + id 'org.igniterealtime.smack.java-common-conventions' + id 'org.igniterealtime.smack.android-conventions' +} + description = """\ DNS SRV with dnsjava Use dnsjava for DNS SRV lookups.""" diff --git a/smack-resolver-javax/build.gradle b/smack-resolver-javax/build.gradle index 4caefb6f5..f724be154 100644 --- a/smack-resolver-javax/build.gradle +++ b/smack-resolver-javax/build.gradle @@ -1,3 +1,7 @@ +plugins { + id 'org.igniterealtime.smack.java-common-conventions' +} + description = """\ DNS SRV with Java7 Use javax.naming for DNS SRV lookups. The javax.naming API is availabe in JavaSE diff --git a/smack-resolver-javax/src/main/java/org/jivesoftware/smack/util/dns/javax/JavaxResolver.java b/smack-resolver-javax/src/main/java/org/jivesoftware/smack/util/dns/javax/JavaxResolver.java index ea508f0fe..1ca3b5721 100644 --- a/smack-resolver-javax/src/main/java/org/jivesoftware/smack/util/dns/javax/JavaxResolver.java +++ b/smack-resolver-javax/src/main/java/org/jivesoftware/smack/util/dns/javax/JavaxResolver.java @@ -1,6 +1,6 @@ /** * - * Copyright 2013-2020 Florian Schmaus + * Copyright 2013-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. @@ -40,11 +40,12 @@ import org.minidns.record.SRV; /** * A DNS resolver (mostly for SRV records), which makes use of the API provided in the javax.* namespace. - * Note that using JavaxResovler requires applications using newer Java versions (at least 11) to declare a dependency on the "sun.jdk" module. + * Note that using JavaxResolver requires applications using newer Java versions (at least 11) to declare a dependency on the "sun.jdk" module. * * @author Florian Schmaus * */ +@SuppressWarnings("JdkObsolete") public class JavaxResolver extends DNSResolver implements SmackInitializer { private static JavaxResolver instance; @@ -83,6 +84,7 @@ public class JavaxResolver extends DNSResolver implements SmackInitializer { } @Override + @SuppressWarnings("BanJNDI") protected List lookupSrvRecords0(DnsName name, List lookupFailures, DnssecMode dnssecMode) { Attribute srvAttribute; diff --git a/smack-resolver-minidns-dox/build.gradle b/smack-resolver-minidns-dox/build.gradle index de740cf84..f8ebf7d3b 100644 --- a/smack-resolver-minidns-dox/build.gradle +++ b/smack-resolver-minidns-dox/build.gradle @@ -1,3 +1,8 @@ +plugins { + id 'org.igniterealtime.smack.java-common-conventions' + id 'org.igniterealtime.smack.android-conventions' +} + description = """\ DNS over XMPP (DoX) support using MiniDNS.""" diff --git a/smack-resolver-minidns/build.gradle b/smack-resolver-minidns/build.gradle index 22809986c..c1fb68a28 100644 --- a/smack-resolver-minidns/build.gradle +++ b/smack-resolver-minidns/build.gradle @@ -1,3 +1,8 @@ +plugins { + id 'org.igniterealtime.smack.java-common-conventions' + id 'org.igniterealtime.smack.android-conventions' +} + description = """\ DNS SRV with minidns Use minidns for DNS SRV lookups. For platforms that don't provide the diff --git a/smack-sasl-javax/build.gradle b/smack-sasl-javax/build.gradle index e6fffd631..9f2eecc89 100644 --- a/smack-sasl-javax/build.gradle +++ b/smack-sasl-javax/build.gradle @@ -1,3 +1,7 @@ +plugins { + id 'org.igniterealtime.smack.java-common-conventions' +} + description = """\ SASL with javax.security.sasl Use javax.security.sasl for SASL.""" diff --git a/smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax/SASLExternalMechanism.java b/smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax/SASLExternalMechanism.java index f0c409c49..fdc7822a8 100644 --- a/smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax/SASLExternalMechanism.java +++ b/smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax/SASLExternalMechanism.java @@ -24,7 +24,7 @@ package org.jivesoftware.smack.sasl.javax; * to the implementer to determine how to do this. Here is one method: * * Create a java keystore with your SSL certificate in it: - * keytool -genkey -alias username -dname "cn=username,ou=organizationalUnit,o=organizationaName,l=locality,s=state,c=country" + * keytool -genkey -alias username -dname "cn=username,ou=organizationalUnit,o=organizationalName,l=locality,s=state,c=country" * * Next, set the System Properties: *
      @@ -38,7 +38,7 @@ package org.jivesoftware.smack.sasl.javax; * simply provide the one in the keyStore. * * Also worth noting is the EXTERNAL mechanism in Smack is not enabled by default. - * To enable it, the implementer will need to call SASLAuthentication.supportSASLMechamism("EXTERNAL"); + * To enable it, the implementer will need to call SASLAuthentication.supportSASLMechanism("EXTERNAL"); * * @author Jay Kline */ diff --git a/smack-sasl-provided/build.gradle b/smack-sasl-provided/build.gradle index a9f3724d0..65f10b3b1 100644 --- a/smack-sasl-provided/build.gradle +++ b/smack-sasl-provided/build.gradle @@ -1,3 +1,8 @@ +plugins { + id 'org.igniterealtime.smack.java-common-conventions' + id 'org.igniterealtime.smack.android-conventions' +} + description = """\ SASL with Smack provided code Use Smack provided code for SASL.""" diff --git a/smack-sasl-provided/src/main/java/org/jivesoftware/smack/sasl/provided/SASLDigestMD5Mechanism.java b/smack-sasl-provided/src/main/java/org/jivesoftware/smack/sasl/provided/SASLDigestMD5Mechanism.java index 2b1a89d0a..284ef767b 100644 --- a/smack-sasl-provided/src/main/java/org/jivesoftware/smack/sasl/provided/SASLDigestMD5Mechanism.java +++ b/smack-sasl-provided/src/main/java/org/jivesoftware/smack/sasl/provided/SASLDigestMD5Mechanism.java @@ -30,7 +30,7 @@ public class SASLDigestMD5Mechanism extends SASLMechanism { public static final String NAME = DIGESTMD5; - private static final String INITAL_NONCE = "00000001"; + private static final String INITIAL_NONCE = "00000001"; /** * The only 'qop' value supported by this implementation @@ -159,7 +159,7 @@ public class SASLDigestMD5Mechanism extends SASLMechanism { + ",realm=\"" + serviceName + '"' + ",nonce=\"" + nonce + '"' + ",cnonce=\"" + cnonce + '"' - + ",nc=" + INITAL_NONCE + + ",nc=" + INITIAL_NONCE + ",qop=auth" + ",digest-uri=\"" + digestUri + '"' + ",response=" + responseValue @@ -218,7 +218,7 @@ public class SASLDigestMD5Mechanism extends SASLMechanism { kd_argument.append(':'); kd_argument.append(nonce); kd_argument.append(':'); - kd_argument.append(INITAL_NONCE); + kd_argument.append(INITIAL_NONCE); kd_argument.append(':'); kd_argument.append(cnonce); kd_argument.append(':'); diff --git a/smack-streammanagement/build.gradle b/smack-streammanagement/build.gradle index c06c423d9..73eb2139e 100644 --- a/smack-streammanagement/build.gradle +++ b/smack-streammanagement/build.gradle @@ -1,3 +1,8 @@ +plugins { + id 'org.igniterealtime.smack.java-common-conventions' + id 'org.igniterealtime.smack.android-conventions' +} + description = """\ Smack support for XMPP Stream Management (XEP-0198).""" diff --git a/smack-tcp/build.gradle b/smack-tcp/build.gradle index a0cffad6b..db2ee6bc2 100644 --- a/smack-tcp/build.gradle +++ b/smack-tcp/build.gradle @@ -1,3 +1,8 @@ +plugins { + id 'org.igniterealtime.smack.java-common-conventions' + id 'org.igniterealtime.smack.android-conventions' +} + description = """\ Smack for standard XMPP connections over TCP.""" diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/sm/predicates/tcp/package-info.java b/smack-tcp/src/main/java/org/jivesoftware/smack/sm/predicates/tcp/package-info.java index 15e022848..d6f94b910 100644 --- a/smack-tcp/src/main/java/org/jivesoftware/smack/sm/predicates/tcp/package-info.java +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/sm/predicates/tcp/package-info.java @@ -16,6 +16,6 @@ */ /** - * XMPPTCPConnection Stream Managment Predicates. + * XMPPTCPConnection Stream Management Predicates. */ package org.jivesoftware.smack.sm.predicates.tcp; diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java index 38d2d3e55..1e622bedc 100644 --- a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java @@ -31,7 +31,6 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.LinkedHashSet; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; @@ -187,7 +186,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { private static boolean useSmResumptionDefault = true; /** - * The stream ID of the stream that is currently resumable, ie. the stream we hold the state + * The stream ID of the stream that is currently resumable, i.e. the stream we hold the state * for in {@link #clientHandledStanzasCount}, {@link #serverHandledStanzasCount} and * {@link #unacknowledgedStanzas}. */ @@ -203,7 +202,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { private Failed smResumptionFailed; /** - * Represents the state of stream magement. + * Represents the state of stream management. *

      * This boolean is marked volatile as it is read by various threads, including the reader thread via {@link #isSmEnabled()}. *

      @@ -295,6 +294,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { * * @param config the connection configuration. */ + @SuppressWarnings("this-escape") public XMPPTCPConnection(XMPPTCPConnectionConfiguration config) { super(config); this.config = config; @@ -415,7 +415,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { // bind IQ may trigger a SM ack request, which would be invalid in the pre resource bound state. smEnabledSyncPoint = false; - List previouslyUnackedStanzas = new LinkedList(); + List previouslyUnackedStanzas = new ArrayList(); if (unacknowledgedStanzas != null) { // There was a previous connection with SM enabled but that was either not resumable or // failed to resume. Make sure that we (re-)send the unacknowledged stanzas. @@ -1162,8 +1162,8 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { } catch (Exception e) { // Set running to false since this thread will exit here and notifyConnectionError() will wait until - // the reader and writer thread's 'running' value is false. Hence we need to set it to false before calling - // notifyConnetctionError() below, even though run() also sets it to false. Therefore, do not remove this. + // the reader and writer thread's 'running' value is false. Hence, we need to set it to false before calling + // notifyConnectionError() below, even though run() also sets it to false. Therefore, do not remove this. running = false; String ignoreReasonThread = null; @@ -1256,8 +1256,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { final boolean smResumptionPossible = isSmResumptionPossible(); // Don't throw a NotConnectedException is there is an resumable stream available if (!smResumptionPossible) { - throw new NotConnectedException(XMPPTCPConnection.this, "done=" + done - + " smResumptionPossible=" + smResumptionPossible); + throw new NotConnectedException(XMPPTCPConnection.this, "done=true smResumptionPossible=false"); } } } @@ -1645,8 +1644,8 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { private void sendSmAcknowledgementInternal() throws NotConnectedException, InterruptedException { AckAnswer ackAnswer = new AckAnswer(clientHandledStanzasCount); // Do net put an ack to the queue if it has already been shutdown. Some servers, like ejabberd, like to request - // an ack even after we have send a stream close (and hance the queue was shutdown). If we would not check here, - // then the ack would dangle around in the queue, and be send on the next re-connection attempt even before the + // an ack even after we have sent a stream close (and hence the queue was shutdown). If we would not check here, + // then the ack would dangle around in the queue, and be sent on the next re-connection attempt even before the // stream open. packetWriter.queue.putIfNotShutdown(ackAnswer); } diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppTcpTransportModule.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppTcpTransportModule.java index bd194fcda..fabfa4d6f 100644 --- a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppTcpTransportModule.java +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppTcpTransportModule.java @@ -1,6 +1,6 @@ /** * - * Copyright 2019-2021 Florian Schmaus + * Copyright 2019-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. @@ -369,11 +369,11 @@ public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionM newPendingOutputFilterData |= outputResult.pendingFilterData; outputFilterInputData = outputResult.filteredOutputData; if (outputFilterInputData != null) { - outputFilterInputData.flip(); + ((java.nio.Buffer) outputFilterInputData).flip(); } } - // It is ok if outpuFilterInputData is 'null' here, this is expected behavior. + // It is ok if outputFilterInputData is 'null' here, this is expected behavior. if (outputFilterInputData != null && outputFilterInputData.hasRemaining()) { filteredOutgoingBuffer = outputFilterInputData; } else { @@ -459,7 +459,7 @@ public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionM } int bytesRead; - incomingBuffer.clear(); + ((java.nio.Buffer) incomingBuffer).clear(); try { bytesRead = selectedSocketChannel.read(incomingBuffer); } catch (IOException e) { @@ -474,7 +474,7 @@ public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionM // read() may return -1 if the input side of a socket is shut down. // Note that we do not call notifyConnectionError() here because the connection may be // cleanly shutdown which would also cause read() to return '-1. I assume that this socket - // will be selected again, on which read() would throw an IOException, which will be catched + // will be selected again, on which read() would throw an IOException, which will be caught // and invoke notifyConnectionError() (see a few lines above). /* IOException exception = new IOException("NIO read() returned " + bytesRead); @@ -503,7 +503,7 @@ public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionM ByteBuffer filteredIncomingBuffer = incomingBuffer; for (ListIterator it = connectionInternal.getXmppInputOutputFilterEndIterator(); it.hasPrevious();) { - filteredIncomingBuffer.flip(); + ((java.nio.Buffer) filteredIncomingBuffer).flip(); ByteBuffer newFilteredIncomingBuffer; try { @@ -518,7 +518,8 @@ public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionM filteredIncomingBuffer = newFilteredIncomingBuffer; } - final int bytesReadAfterFilter = filteredIncomingBuffer.flip().remaining(); + ((java.nio.Buffer) filteredIncomingBuffer).flip(); + final int bytesReadAfterFilter = filteredIncomingBuffer.remaining(); totalBytesReadAfterFilter += bytesReadAfterFilter; @@ -633,7 +634,7 @@ public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionM @Override protected void loadConnectionEndpoints(LookupConnectionEndpointsSuccess lookupConnectionEndpointsSuccess) { // The API contract stats that we will be given the instance we handed out with lookupConnectionEndpoints, - // which must be of type DiscoveredTcpEndpoints here. Hence if we can not cast it, then there is an internal + // which must be of type DiscoveredTcpEndpoints here. Hence, if we can not cast it, then there is an internal // Smack error. discoveredTcpEndpoints = (DiscoveredTcpEndpoints) lookupConnectionEndpointsSuccess; } @@ -711,7 +712,7 @@ public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionM // Add OP_WRITE to the interested Ops, since we have now new things to write. Note that this may cause // multiple reactor threads to race to the channel selected callback in case we perform this right after - // a select() returned with this selection key in the selected-key set. Hence we use tryLock() in the + // a select() returned with this selection key in the selected-key set. Hence, we use tryLock() in the // channel selected callback to keep the invariant that only exactly one thread is performing the // callback. // Note that we need to perform setInterestedOps() *without* holding the channelSelectedCallbackLock, as @@ -776,7 +777,7 @@ public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionM // TODO: It appears this should be done in a generic way. I'd assume we always // have to wait for stream features after the connection was established. If this is true then consider // moving this into State.AbstractTransport. But I am not yet 100% positive that this is the case for every - // transport. Hence keep it here for now. + // transport. Hence, keep it here for now. connectionInternal.newStreamOpenWaitForFeaturesSequence("stream features after initial connection"); return new TcpSocketConnectedResult(remoteAddress); @@ -980,7 +981,7 @@ public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionM ByteBuffer[] outputDataArray = pendingOutputData.toArray(new ByteBuffer[pendingOutputData.size()]); - myNetData.clear(); + ((java.nio.Buffer) myNetData).clear(); while (true) { SSLEngineResult result; @@ -1037,7 +1038,7 @@ public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionM newCapacity = 2 * myNetData.capacity(); } ByteBuffer newMyNetData = ByteBuffer.allocateDirect(newCapacity); - myNetData.flip(); + ((java.nio.Buffer) myNetData).flip(); newMyNetData.put(myNetData); myNetData = newMyNetData; continue; @@ -1060,12 +1061,12 @@ public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionM int accumulatedDataBytes = pendingInputData.remaining() + inputData.remaining(); accumulatedData = ByteBuffer.allocate(accumulatedDataBytes); accumulatedData.put(pendingInputData) - .put(inputData) - .flip(); + .put(inputData); + ((java.nio.Buffer) accumulatedData).flip(); pendingInputData = null; } - peerAppData.clear(); + ((java.nio.Buffer) peerAppData).clear(); while (true) { SSLEngineResult result; @@ -1090,7 +1091,7 @@ public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionM // A delegated task is asynchronously running. Take care of the remaining accumulatedData. addAsPendingInputData(accumulatedData); // Return here, as the async task created by handleHandshakeStatus will continue calling the - // cannelSelectedCallback. + // channelSelectedCallback. return null; case NEED_UNWRAP: continue; @@ -1114,7 +1115,7 @@ public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionM switch (engineResultStatus) { case OK: // SSLEngine's unwrap() may not consume all bytes from the source buffer. If this is the case, then - // simply perform another unwrap until accumlatedData has no remaining bytes. + // simply perform another unwrap until accumulatedData has no remaining bytes. if (accumulatedData.hasRemaining()) { continue; } @@ -1143,7 +1144,8 @@ public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionM // higher layer. That is, here 'byteBuffer' is typically 'incomingBuffer', which is a direct buffer only // allocated once per connection for performance reasons and hence re-used for read() calls. pendingInputData = ByteBuffer.allocate(byteBuffer.remaining()); - pendingInputData.put(byteBuffer).flip(); + pendingInputData.put(byteBuffer); + ((java.nio.Buffer) pendingInputData).flip(); pendingInputFilterData = pendingInputData.hasRemaining(); } diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppTcpTransportModuleDescriptor.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppTcpTransportModuleDescriptor.java index c48d8c546..0dc5d40a4 100644 --- a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppTcpTransportModuleDescriptor.java +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppTcpTransportModuleDescriptor.java @@ -1,6 +1,6 @@ /** * - * Copyright 2019-2020 Florian Schmaus + * Copyright 2019-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. @@ -65,6 +65,8 @@ public class XmppTcpTransportModuleDescriptor extends ModularXmppClientToServerC public static final class Builder extends ModularXmppClientToServerConnectionModuleDescriptor.Builder { + // Invoked via reflection. + @SuppressWarnings("UnusedMethod") private Builder(ModularXmppClientToServerConnectionConfiguration.Builder connectionConfigurationBuilder) { super(connectionConfigurationBuilder); } diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/rce/RemoteXmppTcpConnectionEndpoints.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/rce/RemoteXmppTcpConnectionEndpoints.java index 2b2514dbb..d46344d93 100644 --- a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/rce/RemoteXmppTcpConnectionEndpoints.java +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/rce/RemoteXmppTcpConnectionEndpoints.java @@ -169,7 +169,7 @@ public class RemoteXmppTcpConnectionEndpoints { * * @param domain the domain. * @param domainType the XMPP domain type, server or client. - * @param lookupFailures a list that will be populated with all failures that oocured during lookup. + * @param lookupFailures a list that will be populated with all failures that occurred during lookup. * @param dnssecMode the DNSSEC mode. * @param dnsResolver the DNS resolver to use. * @return a list of resolved host addresses for this domain. diff --git a/smack-websocket-java11/build.gradle b/smack-websocket-java11/build.gradle index 9439bad20..c019f214c 100644 --- a/smack-websocket-java11/build.gradle +++ b/smack-websocket-java11/build.gradle @@ -1,3 +1,7 @@ +plugins { + id 'org.igniterealtime.smack.java-common-conventions' +} + description = """\ Smack for XMPP connections over WebSocket (RFC 7395) using java.net.http.WebSocket.""" diff --git a/smack-websocket-okhttp/build.gradle b/smack-websocket-okhttp/build.gradle index 6f83f2c83..9e7b2fb46 100644 --- a/smack-websocket-okhttp/build.gradle +++ b/smack-websocket-okhttp/build.gradle @@ -1,3 +1,8 @@ +plugins { + id 'org.igniterealtime.smack.java-common-conventions' + id 'org.igniterealtime.smack.android-conventions' +} + description = """\ Smack for XMPP connections over WebSocket (RFC 7395) using OkHttp.""" diff --git a/smack-websocket/build.gradle b/smack-websocket/build.gradle index 1ef60c233..73dfab23b 100644 --- a/smack-websocket/build.gradle +++ b/smack-websocket/build.gradle @@ -1,3 +1,8 @@ +plugins { + id 'org.igniterealtime.smack.java-common-conventions' + id 'org.igniterealtime.smack.android-conventions' +} + description = """\ Smack for XMPP connections over WebSocket (RFC 7395).""" diff --git a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/WebSocketConnectionAttemptState.java b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/WebSocketConnectionAttemptState.java index 80f58ab42..222aafc65 100644 --- a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/WebSocketConnectionAttemptState.java +++ b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/WebSocketConnectionAttemptState.java @@ -51,7 +51,7 @@ public final class WebSocketConnectionAttemptState { /** * Establish a websocket connection with one of the discoveredRemoteConnectionEndpoints.
      * - * @return {@link AbstractWebSocket} with which connection is establised + * @return {@link AbstractWebSocket} with which connection is established * @throws InterruptedException if the calling thread was interrupted */ @SuppressWarnings({"incomplete-switch", "MissingCasesInEnumSwitch"}) diff --git a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/XmppWebSocketTransportModule.java b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/XmppWebSocketTransportModule.java index 9cfa8fe2f..55236858c 100644 --- a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/XmppWebSocketTransportModule.java +++ b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/XmppWebSocketTransportModule.java @@ -135,7 +135,7 @@ public final class XmppWebSocketTransportModule // TODO: It appears this should be done in a generic way. I'd assume we always // have to wait for stream features after the connection was established. But I - // am not yet 100% positive that this is the case for every transport. Hence keep it here for now(?). + // am not yet 100% positive that this is the case for every transport. Hence, keep it here for now(?). // See also similar comment in XmppTcpTransportModule. // Maybe move this into ConnectedButUnauthenticated state's transitionInto() method? That seems to be the // right place. @@ -157,7 +157,7 @@ public final class XmppWebSocketTransportModule final WebSocketRemoteConnectionEndpoint connectedEndpoint; public WebSocketConnectedResult(WebSocketRemoteConnectionEndpoint connectedEndpoint) { - super("WebSocket connection establised with endpoint: " + connectedEndpoint); + super("WebSocket connection established with endpoint: " + connectedEndpoint); this.connectedEndpoint = connectedEndpoint; } } @@ -271,6 +271,7 @@ public final class XmppWebSocketTransportModule asyncButOrderedOutgoingElementsQueue.performAsyncButOrdered(outgoingElementsQueue, () -> { for (TopLevelStreamElement topLevelStreamElement; (topLevelStreamElement = outgoingElementsQueue.poll()) != null;) { websocket.send(topLevelStreamElement); + connectionInternal.fireFirstLevelElementSendListeners(topLevelStreamElement); } }); } 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/main/java/org/jivesoftware/smack/websocket/rce/WebSocketRemoteConnectionEndpointLookup.java b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/rce/WebSocketRemoteConnectionEndpointLookup.java index 347180f69..658814916 100644 --- a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/rce/WebSocketRemoteConnectionEndpointLookup.java +++ b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/rce/WebSocketRemoteConnectionEndpointLookup.java @@ -77,7 +77,7 @@ public final class WebSocketRemoteConnectionEndpointLookup { public Result(List lookupFailures) { // The list of endpoints needs to be mutable, because maybe a user supplied endpoint will be added to it. - // Hence we do not use Collections.emptyList() as argument for the discovered endpoints. + // Hence, we do not use Collections.emptyList() as argument for the discovered endpoints. this(new ArrayList<>(1), new ArrayList<>(1), lookupFailures); } @@ -99,7 +99,7 @@ public final class WebSocketRemoteConnectionEndpointLookup { // TODO: Remove the following methods since the fields are already public? Or make the fields private and use // the methods? I tend to remove the methods, as their method name is pretty long. But OTOH the fields reference - // mutable datastructes, which is uncommon to be public. + // mutable datastructures, which is uncommon to be public. public List getDiscoveredSecureRemoteConnectionEndpoints() { return discoveredSecureEndpoints; } 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/smack-xmlparser-stax/build.gradle b/smack-xmlparser-stax/build.gradle index b25ce2e9a..9cc67deda 100644 --- a/smack-xmlparser-stax/build.gradle +++ b/smack-xmlparser-stax/build.gradle @@ -1,7 +1,6 @@ -// Note that this is also declared in the main build.gradle for -// subprojects, but since evaluationDependsOnChildren is enabled we -// need to declare it here too to have bundle{bnd{...}} available -apply plugin: 'biz.aQute.bnd.builder' +plugins { + id 'org.igniterealtime.smack.java-common-conventions' +} description = """\ Smack XML parser using Stax.""" diff --git a/smack-xmlparser-xpp3/build.gradle b/smack-xmlparser-xpp3/build.gradle index f0a9f56c6..8c5063117 100644 --- a/smack-xmlparser-xpp3/build.gradle +++ b/smack-xmlparser-xpp3/build.gradle @@ -1,7 +1,7 @@ -// Note that this is also declared in the main build.gradle for -// subprojects, but since evaluationDependsOnChildren is enabled we -// need to declare it here too to have bundle{bnd{...}} available -apply plugin: 'biz.aQute.bnd.builder' +plugins { + id 'org.igniterealtime.smack.java-common-conventions' + id 'org.igniterealtime.smack.android-conventions' +} description = """\ Smack XML parser using XPP3.""" diff --git a/smack-xmlparser/build.gradle b/smack-xmlparser/build.gradle index 513a2b153..9686ae87b 100644 --- a/smack-xmlparser/build.gradle +++ b/smack-xmlparser/build.gradle @@ -1,7 +1,7 @@ -// Note that this is also declared in the main build.gradle for -// subprojects, but since evaluationDependsOnChildren is enabled we -// need to declare it here too to have bundle{bnd{...}} available -apply plugin: 'biz.aQute.bnd.builder' +plugins { + id 'org.igniterealtime.smack.java-common-conventions' + id 'org.igniterealtime.smack.android-conventions' +} description = """\ Smack XML parser fundamentals""" diff --git a/smack-xmlparser/src/main/java/org/jivesoftware/smack/xml/XmlPullParser.java b/smack-xmlparser/src/main/java/org/jivesoftware/smack/xml/XmlPullParser.java index c49ac66ae..fd5a5acfc 100644 --- a/smack-xmlparser/src/main/java/org/jivesoftware/smack/xml/XmlPullParser.java +++ b/smack-xmlparser/src/main/java/org/jivesoftware/smack/xml/XmlPullParser.java @@ -110,7 +110,7 @@ public interface XmlPullParser { String getAttributeNamespace(int index); /** - * Returns the loacalpart of the attribute's name or null in case the index does not refer to an + * Returns the localpart of the attribute's name or null in case the index does not refer to an * attribute. * * @param index the attribute index. diff --git a/version b/version index 891d39303..da70b5b12 100644 --- a/version +++ b/version @@ -1 +1 @@ -4.5.0-alpha2-SNAPSHOT +4.5.0-beta5-SNAPSHOT