diff --git a/documentation/extensions/index.md b/documentation/extensions/index.md index f84b5c025..da5c08991 100644 --- a/documentation/extensions/index.md +++ b/documentation/extensions/index.md @@ -56,6 +56,7 @@ Smack Extensions and currently supported XEPs of smack-extensions | Advanced Message Processing | [XEP-0079](https://xmpp.org/extensions/xep-0079.html) | n/a | Enables entities to request, and servers to perform, advanced processing of XMPP message stanzas. | | User Location | [XEP-0080](https://xmpp.org/extensions/xep-0080.html) | n/a | Enabled communicating information about the current geographical or physical location of an entity. | | XMPP Date Time Profiles | [XEP-0082](https://xmpp.org/extensions/xep-0082.html) | n/a | Standardization of Date and Time representation in XMPP. | +| User Avatar | [XEP-0084](https://xmpp.org/extensions/xep-0084.html) | 1.1.2 | Allows to exchange user avatars, which are small images or icons associated with human users. | | Chat State Notifications | [XEP-0085](https://xmpp.org/extensions/xep-0085.html) | n/a | Communicating the status of a user in a chat session. | | [Time Exchange](time.md) | [XEP-0090](https://xmpp.org/extensions/xep-0090.html) | n/a | Allows local time information to be shared between users. | | Software Version | [XEP-0092](https://xmpp.org/extensions/xep-0092.html) | n/a | Retrieve and announce the software application of an XMPP entity. | @@ -89,7 +90,6 @@ Smack Extensions and currently supported XEPs of smack-extensions | [Group Chat Invitations](invitation.md) | n/a | n/a | Send invitations to other users to join a group chat room. | | [Jive Properties](properties.md) | n/a | n/a | TODO | - Experimental Smack Extensions and currently supported XEPs of smack-experimental -------------------------------------------------------------------------------- diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..f6b961fd5 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..97fd889ad --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sun Sep 01 01:05:38 CEST 2019 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 000000000..cccdd3d51 --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# 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 +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="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# 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 + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +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" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +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. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +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, switch paths to Windows format before running java +if $cygwin ; 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=$((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" ;; + 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, 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" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 000000000..e95643d6a --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +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. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +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. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="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 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/XmlStringBuilder.java b/smack-core/src/main/java/org/jivesoftware/smack/util/XmlStringBuilder.java index 145b1d3ec..9fd61c57b 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 @@ -19,6 +19,7 @@ package org.jivesoftware.smack.util; import java.io.IOException; import java.io.Writer; import java.util.ArrayList; +import java.net.URL; import java.util.Collection; import java.util.Date; import java.util.List; @@ -358,6 +359,13 @@ public class XmlStringBuilder implements Appendable, CharSequence, Element { return this; } + public XmlStringBuilder optAttribute(String name, URL url) { + if (url != null) { + attribute(name, url.toExternalForm()); + } + return this; + } + /** * Add the given attribute if {@code value => 0}. * diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/AvatarMetadataStore.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/AvatarMetadataStore.java new file mode 100644 index 000000000..6ee213558 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/AvatarMetadataStore.java @@ -0,0 +1,46 @@ +/** + * + * Copyright 2019 Paul Schaub + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.avatar; + +import org.jxmpp.jid.EntityBareJid; + +/** + * The {@link AvatarMetadataStore} interface defines methods used by the {@link UserAvatarManager} to determine, + * whether the client already has a local copy of a published avatar or if the user needs to be informed about the + * update in order to download the image. + */ +public interface AvatarMetadataStore { + + /** + * Determine, if the client already has a copy of the avatar with {@code itemId} available or not. + * + * @param jid {@link EntityBareJid} of the entity that published the avatar. + * @param itemId itemId of the avatar + * + * @return true if the client already has a local copy of the avatar, false otherwise + */ + boolean hasAvatarAvailable(EntityBareJid jid, String itemId); + + /** + * Mark the tuple (jid, itemId) as available. This means that the client already has a local copy of the avatar + * available and wishes not to be notified about this particular avatar anymore. + * + * @param jid {@link EntityBareJid} of the entity that published the avatar. + * @param itemId itemId of the avatar + */ + void setAvatarAvailable(EntityBareJid jid, String itemId); +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/MetadataInfo.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/MetadataInfo.java new file mode 100644 index 000000000..0144f22f2 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/MetadataInfo.java @@ -0,0 +1,124 @@ +/** + * + * Copyright 2017 Fernando Ramirez, 2019 Paul Schaub + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.avatar; + +import java.net.URL; + +import org.jivesoftware.smack.datatypes.UInt16; +import org.jivesoftware.smack.datatypes.UInt32; +import org.jivesoftware.smack.util.StringUtils; + +/** + * User Avatar metadata info model class. + * + * @author Fernando Ramirez + * @author Paul Schaub + * @see XEP-0084: User + * Avatar + */ +public class MetadataInfo { + + public static final int MAX_HEIGHT = 65536; + public static final int MAX_WIDTH = 65536; + + private final String id; + private final URL url; + private final UInt32 bytes; + private final String type; + private final UInt16 height; + private final UInt16 width; + + /** + * MetadataInfo constructor. + * + * @param id SHA-1 hash of the image data + * @param url http(s) url of the image + * @param bytes size of the image in bytes + * @param type content type of the image + * @param pixelsHeight height of the image in pixels + * @param pixelsWidth width of the image in pixels + */ + public MetadataInfo(String id, URL url, long bytes, String type, int pixelsHeight, int pixelsWidth) { + this.id = StringUtils.requireNotNullNorEmpty(id, "ID is required."); + this.url = url; + this.bytes = UInt32.from(bytes); + this.type = StringUtils.requireNotNullNorEmpty(type, "Content Type is required."); + if (pixelsHeight < 0 || pixelsHeight > MAX_HEIGHT) { + throw new IllegalArgumentException("Image height value must be between 0 and 65536."); + } + if (pixelsWidth < 0 || pixelsWidth > MAX_WIDTH) { + throw new IllegalArgumentException("Image width value must be between 0 and 65536."); + } + this.height = UInt16.from(pixelsHeight); + this.width = UInt16.from(pixelsWidth); + } + + /** + * Get the id. + * + * @return the id + */ + public String getId() { + return id; + } + + /** + * Get the url of the avatar image. + * + * @return the url + */ + public URL getUrl() { + return url; + } + + /** + * Get the amount of bytes. + * + * @return the amount of bytes + */ + public UInt32 getBytes() { + return bytes; + } + + /** + * Get the type. + * + * @return the type + */ + public String getType() { + return type; + } + + /** + * Get the height in pixels. + * + * @return the height in pixels + */ + public UInt16 getHeight() { + return height; + } + + /** + * Get the width in pixels. + * + * @return the width in pixels + */ + public UInt16 getWidth() { + return width; + } + +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/MetadataPointer.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/MetadataPointer.java new file mode 100644 index 000000000..6f2cff974 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/MetadataPointer.java @@ -0,0 +1,86 @@ +/** + * + * Copyright 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. + * 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.avatar; + +import java.util.Map; + +import org.jivesoftware.smack.util.StringUtils; + +/** + * User Avatar metadata pointer model class. + * A pointer element is used to point to an avatar which is not published via PubSub or HTTP, but provided by a + * third-party service. + * + * @author Fernando Ramirez + * @see XEP-0084: User Avatar + */ +public class MetadataPointer { + + private final String namespace; + private final Map fields; + + /** + * Metadata Pointer constructor. + * + * The following example + *
+     * {@code
+     * 
+     *     
+     *         Ancapistan
+     *         Kropotkin
+     *     
+     * 
+     * }
+     * 
+ * can be created by constructing the object like this: + *
+     * {@code
+     *     Map fields = new HashMap<>();
+     *     fields.add("game", "Ancapistan");
+     *     fields.add("character", "Kropotkin");
+     *     MetadataPointer pointer = new MetadataPointer("http://example.com/virtualworlds", fields);
+     * }
+     * 
+ * + * @param namespace namespace of the child element of the metadata pointer. + * @param fields fields of the child element as key, value pairs. + */ + public MetadataPointer(String namespace, Map fields) { + this.namespace = StringUtils.requireNotNullNorEmpty(namespace, "Namespace MUST NOT be null, nor empty."); + this.fields = fields; + } + + /** + * Get the namespace of the pointers child element. + * + * @return the namespace + */ + public String getNamespace() { + return namespace; + } + + /** + * Get the fields of the pointers child element. + * + * @return the fields + */ + public Map getFields() { + return fields; + } + +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/UserAvatarManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/UserAvatarManager.java new file mode 100644 index 000000000..089157e4d --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/UserAvatarManager.java @@ -0,0 +1,411 @@ +/** + * + * Copyright 2017 Fernando Ramirez, 2019 Paul Schaub + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.avatar; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; + +import org.jivesoftware.smack.Manager; +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.ExtensionElement; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.util.SHA1; +import org.jivesoftware.smack.util.stringencoder.Base64; +import org.jivesoftware.smackx.avatar.element.DataExtension; +import org.jivesoftware.smackx.avatar.element.MetadataExtension; +import org.jivesoftware.smackx.avatar.listener.AvatarListener; +import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; +import org.jivesoftware.smackx.pep.PepListener; +import org.jivesoftware.smackx.pep.PepManager; +import org.jivesoftware.smackx.pubsub.EventElement; +import org.jivesoftware.smackx.pubsub.ItemsExtension; +import org.jivesoftware.smackx.pubsub.LeafNode; +import org.jivesoftware.smackx.pubsub.PayloadItem; +import org.jivesoftware.smackx.pubsub.PubSubException; +import org.jivesoftware.smackx.pubsub.PubSubManager; + +import org.jxmpp.jid.EntityBareJid; + +/** + * User Avatar manager class. + * + * @author Fernando Ramirez + * @author Paul Schaub + * @see XEP-0084: User + * Avatar + */ +public final class UserAvatarManager extends Manager { + + public static final String DATA_NAMESPACE = "urn:xmpp:avatar:data"; + public static final String METADATA_NAMESPACE = "urn:xmpp:avatar:metadata"; + public static final String FEATURE_METADATA = METADATA_NAMESPACE + "+notify"; + + private static final Map INSTANCES = new WeakHashMap<>(); + + private final PepManager pepManager; + private final ServiceDiscoveryManager serviceDiscoveryManager; + + private AvatarMetadataStore metadataStore; + private final Set avatarListeners = new HashSet<>(); + + /** + * Get the singleton instance of UserAvatarManager. + * + * @param connection {@link XMPPConnection}. + * @return the instance of UserAvatarManager + */ + public static synchronized UserAvatarManager getInstanceFor(XMPPConnection connection) { + UserAvatarManager userAvatarManager = INSTANCES.get(connection); + + if (userAvatarManager == null) { + userAvatarManager = new UserAvatarManager(connection); + INSTANCES.put(connection, userAvatarManager); + } + + return userAvatarManager; + } + + private UserAvatarManager(XMPPConnection connection) { + super(connection); + this.pepManager = PepManager.getInstanceFor(connection); + this.serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection); + } + + /** + * Returns true if User Avatar publishing is supported by the server. + * In order to support User Avatars the server must have support for XEP-0163: Personal Eventing Protocol (PEP). + * + * @return true if User Avatar is supported by the server. + * + * @see XEP-0163: Personal Eventing Protocol + * + * @throws NoResponseException + * @throws XMPPErrorException + * @throws NotConnectedException + * @throws InterruptedException + */ + public boolean isSupportedByServer() + throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { + return pepManager.isSupported(); + } + + /** + * Announce support for User Avatars and start receiving avatar updates. + */ + public void enable() { + pepManager.addPepListener(metadataExtensionListener); + serviceDiscoveryManager.addFeature(FEATURE_METADATA); + } + + /** + * Stop receiving avatar updates. + */ + public void disable() { + serviceDiscoveryManager.removeFeature(FEATURE_METADATA); + pepManager.addPepListener(metadataExtensionListener); + } + + /** + * Set an {@link AvatarMetadataStore} which is used to store information about the local availability of avatar + * data. + * @param metadataStore metadata store + */ + public void setAvatarMetadataStore(AvatarMetadataStore metadataStore) { + this.metadataStore = metadataStore; + } + + /** + * Register an {@link AvatarListener} in order to be notified about incoming avatar metadata updates. + * + * @param listener listener + * @return true if the set of listeners did not already contain the listener + */ + public synchronized boolean addAvatarListener(AvatarListener listener) { + return avatarListeners.add(listener); + } + + /** + * Unregister an {@link AvatarListener} to stop being notified about incoming avatar metadata updates. + * + * @param listener listener + * @return true if the set of listeners contained the listener + */ + public synchronized boolean removeAvatarListener(AvatarListener listener) { + return avatarListeners.remove(listener); + } + + /** + * Get the data node. + * This node contains the avatar image data. + * + * @return the data node + * @throws NoResponseException + * @throws NotConnectedException + * @throws InterruptedException + * @throws XMPPErrorException + */ + private LeafNode getOrCreateDataNode() + throws NoResponseException, NotConnectedException, InterruptedException, XMPPErrorException, PubSubException.NotALeafNodeException { + return pepManager.getPepPubSubManager().getOrCreateLeafNode(DATA_NAMESPACE); + } + + /** + * Get the metadata node. + * This node contains lightweight metadata information about the data in the data node. + * + * @return the metadata node + * @throws NoResponseException + * @throws NotConnectedException + * @throws InterruptedException + * @throws XMPPErrorException + */ + private LeafNode getOrCreateMetadataNode() + throws NoResponseException, NotConnectedException, InterruptedException, XMPPErrorException, PubSubException.NotALeafNodeException { + return pepManager.getPepPubSubManager().getOrCreateLeafNode(METADATA_NAMESPACE); + } + + /** + * Publish a PNG Avatar and its metadata to PubSub. + * + * @param data + * @param height + * @param width + * @throws XMPPErrorException + * @throws PubSubException.NotALeafNodeException + * @throws NotConnectedException + * @throws InterruptedException + * @throws NoResponseException + */ + public void publishAvatar(byte[] data, int height, int width) + throws XMPPErrorException, PubSubException.NotALeafNodeException, NotConnectedException, + InterruptedException, NoResponseException { + String id = publishAvatarData(data); + publishAvatarMetadata(id, data.length, "image/png", height, width); + } + + /** + * Publish a PNG avatar and its metadata to PubSub. + * + * @param pngFile PNG File + * @param height height of the image + * @param width width of the image + * + * @throws IOException + * @throws XMPPErrorException + * @throws PubSubException.NotALeafNodeException + * @throws NotConnectedException + * @throws InterruptedException + * @throws NoResponseException + */ + public void publishAvatar(File pngFile, int height, int width) + throws IOException, XMPPErrorException, PubSubException.NotALeafNodeException, NotConnectedException, + InterruptedException, NoResponseException { + try (ByteArrayOutputStream out = new ByteArrayOutputStream((int) pngFile.length()); + InputStream in = new BufferedInputStream(new FileInputStream(pngFile))) { + byte[] buffer = new byte[4096]; + int read; + while ((read = in.read(buffer)) != -1) { + out.write(buffer, 0, read); + } + byte[] bytes = out.toByteArray(); + publishAvatar(bytes, height, width); + } + } + + public byte[] fetchAvatarFromPubSub(EntityBareJid from, MetadataInfo metadataInfo) + throws InterruptedException, PubSubException.NotALeafNodeException, NoResponseException, + NotConnectedException, XMPPErrorException, PubSubException.NotAPubSubNodeException { + LeafNode dataNode = PubSubManager.getInstanceFor(connection(), from) + .getLeafNode(DATA_NAMESPACE); + + List> dataItems = dataNode.getItems(1, metadataInfo.getId()); + DataExtension extension = dataItems.get(0).getPayload(); + if (metadataStore != null) { + metadataStore.setAvatarAvailable(from, metadataInfo.getId()); + } + return extension.getData(); + } + + private String publishAvatarData(byte[] data) + throws NoResponseException, NotConnectedException, XMPPErrorException, InterruptedException, PubSubException.NotALeafNodeException { + String itemId = Base64.encodeToString(SHA1.bytes(data)); + publishAvatarData(data, itemId); + return itemId; + } + + private void publishAvatarData(byte[] data, String itemId) + throws NoResponseException, NotConnectedException, XMPPErrorException, InterruptedException, PubSubException.NotALeafNodeException { + DataExtension dataExtension = new DataExtension(data); + getOrCreateDataNode().publish(new PayloadItem<>(itemId, dataExtension)); + } + + /** + * Publish metadata about an avatar to the metadata node. + * + * @param itemId SHA-1 sum of the image of type image/png + * @param info info element containing metadata of the file + * @param pointers list of metadata pointer elements + * + * @throws NoResponseException + * @throws XMPPErrorException + * @throws NotConnectedException + * @throws InterruptedException + */ + public void publishAvatarMetadata(String itemId, MetadataInfo info, List pointers) + throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, PubSubException.NotALeafNodeException { + publishAvatarMetadata(itemId, Collections.singletonList(info), pointers); + } + + /** + * Publish avatar metadata. + * + * @param itemId SHA-1 sum of the avatar image representation of type image/png + * @param infos list of metadata elements + * @param pointers list of pointer elements + * + * @throws NoResponseException + * @throws XMPPErrorException + * @throws NotConnectedException + * @throws InterruptedException + */ + public void publishAvatarMetadata(String itemId, List infos, List pointers) + throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, PubSubException.NotALeafNodeException { + MetadataExtension metadataExtension = new MetadataExtension(infos, pointers); + getOrCreateMetadataNode().publish(new PayloadItem<>(itemId, metadataExtension)); + + if (metadataStore == null) { + return; + } + // Mark our own avatar as locally available so that we don't get updates for it + metadataStore.setAvatarAvailable(connection().getUser().asEntityBareJidOrThrow(), itemId); + } + + /** + * Publish metadata about an avatar available via HTTP. + * This method can be used together with HTTP File Upload as an alternative to PubSub for avatar publishing. + * + * @param itemId SHA-1 sum of the avatar image file. + * @param url HTTP(S) Url of the image file. + * @param bytes size of the file in bytes + * @param type content type of the file + * @param pixelsHeight height of the image file in pixels + * @param pixelsWidth width of the image file in pixels + * + * @throws NoResponseException + * @throws XMPPErrorException + * @throws NotConnectedException + * @throws InterruptedException + */ + public void publishHttpAvatarMetadata(String itemId, URL url, long bytes, String type, + int pixelsHeight, int pixelsWidth) + throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, PubSubException.NotALeafNodeException { + MetadataInfo info = new MetadataInfo(itemId, url, bytes, type, pixelsHeight, pixelsWidth); + publishAvatarMetadata(itemId, info, null); + } + + /** + * Publish avatar metadata with its size in pixels. + * + * @param itemId + * @param bytes + * @param type + * @param pixelsHeight + * @param pixelsWidth + * @throws NoResponseException + * @throws XMPPErrorException + * @throws NotConnectedException + * @throws InterruptedException + */ + public void publishAvatarMetadata(String itemId, long bytes, String type, int pixelsHeight, + int pixelsWidth) + throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, PubSubException.NotALeafNodeException { + MetadataInfo info = new MetadataInfo(itemId, null, bytes, type, pixelsHeight, pixelsWidth); + publishAvatarMetadata(itemId, info, null); + } + + /** + * Publish an empty metadata element to disable avatar publishing. + * + * @see ยง4.2 Metadata Element + * + * @throws NoResponseException + * @throws XMPPErrorException + * @throws NotConnectedException + * @throws InterruptedException + */ + public void unpublishAvatar() + throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, PubSubException.NotALeafNodeException { + getOrCreateMetadataNode().publish(new PayloadItem<>(new MetadataExtension(null))); + } + + private final PepListener metadataExtensionListener = new PepListener() { + @Override + public void eventReceived(EntityBareJid from, EventElement event, Message message) { + if (!MetadataExtension.NAMESPACE.equals(event.getNamespace())) { + // Totally not of interest for us. + return; + } + + if (!MetadataExtension.ELEMENT.equals(event.getElementName())) { + return; + } + + for (ExtensionElement items : event.getExtensions()) { + if (!(items instanceof ItemsExtension)) { + continue; + } + + for (ExtensionElement item : ((ItemsExtension) items).getExtensions()) { + if (!(item instanceof PayloadItem)) { + continue; + } + + PayloadItem payloadItem = (PayloadItem) item; + + if (!(payloadItem.getPayload() instanceof MetadataExtension)) { + continue; + } + + MetadataExtension metadataExtension = (MetadataExtension) payloadItem.getPayload(); + if (metadataStore != null && metadataStore.hasAvatarAvailable(from, ((PayloadItem) item).getId())) { + // The metadata store implies that we have a local copy of the published image already. Skip. + continue; + } + + for (AvatarListener listener : avatarListeners) { + listener.onAvatarUpdateReceived(from, metadataExtension); + } + } + } + } + }; + +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/element/DataExtension.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/element/DataExtension.java new file mode 100644 index 000000000..649e102d7 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/element/DataExtension.java @@ -0,0 +1,102 @@ +/** + * + * Copyright 2017 Fernando Ramirez, 2019 Paul Schaub + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.avatar.element; + +import org.jivesoftware.smack.packet.ExtensionElement; +import org.jivesoftware.smack.packet.XmlEnvironment; +import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smack.util.XmlStringBuilder; +import org.jivesoftware.smack.util.stringencoder.Base64; +import org.jivesoftware.smackx.avatar.UserAvatarManager; + +/** + * Data extension element class used to publish avatar image data via PubSub. + * Unlike the {@link MetadataExtension}, this class is dedicated to containing the avatar image data itself. + * + * @author Fernando Ramirez + * @see XEP-0084: User + * Avatar + */ +public class DataExtension implements ExtensionElement { + + public static final String ELEMENT = "data"; + public static final String NAMESPACE = UserAvatarManager.DATA_NAMESPACE; + + private final byte[] data; + private String data_b64; // lazy initialized base64 encoded copy of data + + /** + * Create a {@link DataExtension} from a byte array. + * + * @param data bytes of the image. + */ + public DataExtension(byte[] data) { + this.data = data; + } + + /** + * Create a {@link DataExtension} from a base64 encoded String. + * + * @param base64data bytes of the image as base64 string. + */ + public DataExtension(String base64data) { + this.data_b64 = StringUtils.requireNotNullNorEmpty(base64data, + "Base64 String MUST NOT be null, nor empty."); + this.data = Base64.decode(base64data); + } + + /** + * Get the bytes of the image. + * + * @return an immutable copy of the image data + */ + public byte[] getData() { + return data.clone(); + } + + /** + * Get the image data encoded as a base64 String. + * + * @return the data as String + */ + public String getDataAsString() { + if (data_b64 == null) { + data_b64 = Base64.encodeToString(data); + } + return data_b64; + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public String getNamespace() { + return NAMESPACE; + } + + @Override + public CharSequence toXML(XmlEnvironment xmlEnvironment) { + XmlStringBuilder xml = new XmlStringBuilder(this); + xml.rightAngleBracket(); + xml.escape(this.getDataAsString()); + xml.closeElement(this); + return xml; + } + +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/element/MetadataExtension.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/element/MetadataExtension.java new file mode 100644 index 000000000..6f95d66f5 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/element/MetadataExtension.java @@ -0,0 +1,173 @@ +/** + * + * Copyright 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. + * 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.avatar.element; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.jivesoftware.smack.packet.ExtensionElement; +import org.jivesoftware.smack.packet.Stanza; +import org.jivesoftware.smack.packet.XmlEnvironment; +import org.jivesoftware.smack.util.XmlStringBuilder; +import org.jivesoftware.smackx.avatar.MetadataInfo; +import org.jivesoftware.smackx.avatar.MetadataPointer; +import org.jivesoftware.smackx.avatar.UserAvatarManager; + +/** + * Metadata extension element class. + * This class contains metadata about published avatars. + * + * @author Fernando Ramirez + * @see XEP-0084: User + * Avatar + */ +public class MetadataExtension implements ExtensionElement { + + public static final String ELEMENT = "metadata"; + public static final String NAMESPACE = UserAvatarManager.METADATA_NAMESPACE; + + private final List infos; + private final List pointers; + + /** + * Metadata Extension constructor. + * + * @param infos + */ + public MetadataExtension(List infos) { + this(infos, null); + } + + /** + * Metadata Extension constructor. + * + * @param infos + * @param pointers + */ + public MetadataExtension(List infos, List pointers) { + this.infos = infos; + this.pointers = pointers; + } + + /** + * Get the info elements list. + * + * @return the info elements list + */ + public List getInfoElements() { + return Collections.unmodifiableList(infos); + } + + /** + * Get the pointer elements list. + * + * @return the pointer elements list + */ + public List getPointerElements() { + return (pointers == null) ? null : Collections.unmodifiableList(pointers); + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public String getNamespace() { + return NAMESPACE; + } + + @Override + public CharSequence toXML(XmlEnvironment xmlEnvironment) { + XmlStringBuilder xml = new XmlStringBuilder(this); + appendInfoElements(xml); + appendPointerElements(xml); + closeElement(xml); + return xml; + } + + private void appendInfoElements(XmlStringBuilder xml) { + if (infos != null) { + xml.rightAngleBracket(); + + for (MetadataInfo info : infos) { + xml.halfOpenElement("info"); + xml.attribute("id", info.getId()); + xml.attribute("bytes", info.getBytes().longValue()); + xml.attribute("type", info.getType()); + xml.optAttribute("url", info.getUrl()); + + if (info.getHeight().nativeRepresentation() > 0) { + xml.attribute("height", info.getHeight().nativeRepresentation()); + } + + if (info.getWidth().nativeRepresentation() > 0) { + xml.attribute("width", info.getWidth().nativeRepresentation()); + } + + xml.closeEmptyElement(); + } + } + } + + private void appendPointerElements(XmlStringBuilder xml) { + if (pointers != null) { + + for (MetadataPointer pointer : pointers) { + xml.openElement("pointer"); + xml.halfOpenElement("x"); + + String namespace = pointer.getNamespace(); + if (namespace != null) { + xml.xmlnsAttribute(namespace); + } + + xml.rightAngleBracket(); + + Map fields = pointer.getFields(); + if (fields != null) { + for (Map.Entry pair : fields.entrySet()) { + xml.escapedElement(pair.getKey(), String.valueOf(pair.getValue())); + } + } + + xml.closeElement("x"); + xml.closeElement("pointer"); + } + + } + } + + private void closeElement(XmlStringBuilder xml) { + if (infos != null || pointers != null) { + xml.closeElement(this); + } else { + xml.closeEmptyElement(); + } + } + + /** + * Return true, if this {@link MetadataExtension} is to be interpreted as Avatar unpublishing. + * + * @return true if unpublishing, false otherwise + */ + public boolean isDisablingPublishing() { + return getInfoElements().isEmpty(); + } + +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/element/package-info.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/element/package-info.java new file mode 100644 index 000000000..b55f1aa91 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/element/package-info.java @@ -0,0 +1,24 @@ +/** + * + * Copyright 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. + * 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. + */ +/** + * User Avatar elements. + * + * @author Fernando Ramirez + * @see XEP-0084: User + * Avatar + */ +package org.jivesoftware.smackx.avatar.element; diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/listener/AvatarListener.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/listener/AvatarListener.java new file mode 100644 index 000000000..bd657a3e5 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/listener/AvatarListener.java @@ -0,0 +1,31 @@ +/** + * + * Copyright 2019 Paul Schaub + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.avatar.listener; + +import org.jivesoftware.smackx.avatar.element.MetadataExtension; + +import org.jxmpp.jid.EntityBareJid; + +/** + * Listener that can notify the user about User Avatar updates. + * + * @author Paul Schaub + */ +public interface AvatarListener { + + void onAvatarUpdateReceived(EntityBareJid user, MetadataExtension metadata); +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/listener/package-info.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/listener/package-info.java new file mode 100644 index 000000000..160328bb4 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/listener/package-info.java @@ -0,0 +1,24 @@ +/** + * + * Copyright 2019 Paul Schaub + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Classes and interfaces of User Avatar. + * + * @author Paul Schaub + * @see XEP-0084: User + * Avatar + */ +package org.jivesoftware.smackx.avatar.listener; diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/package-info.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/package-info.java new file mode 100644 index 000000000..67f3ef5c6 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/package-info.java @@ -0,0 +1,24 @@ +/** + * + * Copyright 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. + * 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. + */ +/** + * Classes and interfaces of User Avatar. + * + * @author Fernando Ramirez + * @see XEP-0084: User + * Avatar + */ +package org.jivesoftware.smackx.avatar; diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/provider/DataProvider.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/provider/DataProvider.java new file mode 100644 index 000000000..69de5221d --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/provider/DataProvider.java @@ -0,0 +1,44 @@ +/** + * + * Copyright 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. + * 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.avatar.provider; + +import java.io.IOException; + +import org.jivesoftware.smack.packet.XmlEnvironment; +import org.jivesoftware.smack.provider.ExtensionElementProvider; +import org.jivesoftware.smack.util.stringencoder.Base64; +import org.jivesoftware.smack.xml.XmlPullParser; +import org.jivesoftware.smack.xml.XmlPullParserException; +import org.jivesoftware.smackx.avatar.element.DataExtension; + +/** + * User Avatar data provider class. + * + * @author Fernando Ramirez + * @see XEP-0084: User + * Avatar + */ +public class DataProvider extends ExtensionElementProvider { + + @Override + public DataExtension parse(XmlPullParser parser, int initialDepth, XmlEnvironment environment) + throws IOException, XmlPullParserException { + byte[] data = Base64.decode(parser.nextText()); + return new DataExtension(data); + } + +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/provider/MetadataProvider.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/provider/MetadataProvider.java new file mode 100644 index 000000000..4490bb007 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/provider/MetadataProvider.java @@ -0,0 +1,154 @@ +/** + * + * Copyright 2017 Fernando Ramirez, 2019 Paul Schaub + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.avatar.provider; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import org.jivesoftware.smack.packet.XmlEnvironment; +import org.jivesoftware.smack.provider.ExtensionElementProvider; +import org.jivesoftware.smack.xml.XmlPullParser; +import org.jivesoftware.smack.xml.XmlPullParserException; +import org.jivesoftware.smackx.avatar.MetadataInfo; +import org.jivesoftware.smackx.avatar.MetadataPointer; +import org.jivesoftware.smackx.avatar.element.MetadataExtension; + +/** + * User Avatar metadata provider class. + * + * @author Fernando Ramirez + * @author Paul Schaub + * @see XEP-0084: User + * Avatar + */ +public class MetadataProvider extends ExtensionElementProvider { + + @Override + public MetadataExtension parse(XmlPullParser parser, int initialDepth, XmlEnvironment xmlEnvironment) throws IOException, XmlPullParserException { + List metadataInfos = null; + List pointers = null; + + while (true) { + XmlPullParser.Event eventType = parser.next(); + + if (eventType == XmlPullParser.Event.START_ELEMENT) { + + if (parser.getName().equals("info")) { + if (metadataInfos == null) { + metadataInfos = new ArrayList<>(); + } + + MetadataInfo info = parseInfo(parser); + if (info.getId() != null) { + metadataInfos.add(info); + } + } + + if (parser.getName().equals("pointer")) { + if (pointers == null) { + pointers = new ArrayList<>(); + } + + pointers.add(parsePointer(parser)); + } + + } else if (eventType == XmlPullParser.Event.END_ELEMENT) { + if (parser.getDepth() == initialDepth) { + break; + } + } + } + + return new MetadataExtension(metadataInfos, pointers); + } + + private MetadataInfo parseInfo(XmlPullParser parser) throws XmlPullParserException { + String id; + URL url = null; + long bytes = 0; + String type; + int pixelsHeight = 0; + int pixelsWidth = 0; + + id = parser.getAttributeValue("", "id"); + type = parser.getAttributeValue("", "type"); + String urlString = parser.getAttributeValue("", "url"); + if (urlString != null && !urlString.isEmpty()) { + try { + url = new URL(urlString); + } catch (MalformedURLException e) { + throw new XmlPullParserException("Cannot parse URL '" + urlString + "'"); + } + } + + String bytesString = parser.getAttributeValue("", "bytes"); + if (bytesString != null) { + bytes = Long.parseLong(bytesString); + } + + String widthString = parser.getAttributeValue("", "width"); + if (widthString != null) { + pixelsWidth = Integer.parseInt(widthString); + } + + String heightString = parser.getAttributeValue("", "height"); + if (heightString != null) { + pixelsHeight = Integer.parseInt(heightString); + } + + try { + return new MetadataInfo(id, url, bytes, type, pixelsHeight, pixelsWidth); + } catch (IllegalArgumentException e) { + throw new XmlPullParserException(e); + } + } + + private MetadataPointer parsePointer(XmlPullParser parser) throws XmlPullParserException, IOException { + int pointerDepth = parser.getDepth(); + String namespace = null; + HashMap fields = null; + + while (true) { + XmlPullParser.Event eventType2 = parser.next(); + + if (eventType2 == XmlPullParser.Event.START_ELEMENT) { + if (parser.getName().equals("x")) { + namespace = parser.getNamespace(); + } else { + if (fields == null) { + fields = new HashMap<>(); + } + + String name = parser.getName(); + Object value = parser.nextText(); + fields.put(name, value); + } + } else if (eventType2 == XmlPullParser.Event.END_ELEMENT) { + if (parser.getDepth() == pointerDepth) { + break; + } + } + } + + return new MetadataPointer(namespace, fields); + } + +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/provider/package-info.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/provider/package-info.java new file mode 100644 index 000000000..38f659ac9 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/provider/package-info.java @@ -0,0 +1,24 @@ +/** + * + * Copyright 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. + * 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. + */ +/** + * User Avatar providers. + * + * @author Fernando Ramirez + * @see XEP-0084: User + * Avatar + */ +package org.jivesoftware.smackx.avatar.provider; 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 6d7ebc38c..92476171d 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 @@ -317,6 +317,18 @@ org.jivesoftware.smackx.geoloc.provider.GeoLocationProvider + + + data + urn:xmpp:avatar:data + org.jivesoftware.smackx.avatar.provider.DataProvider + + + metadata + urn:xmpp:avatar:metadata + org.jivesoftware.smackx.avatar.provider.MetadataProvider + + active diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/avatar/DataExtensionTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/avatar/DataExtensionTest.java new file mode 100644 index 000000000..6fb74807b --- /dev/null +++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/avatar/DataExtensionTest.java @@ -0,0 +1,48 @@ +/** + * + * Copyright 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. + * 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.avatar; + +import org.jivesoftware.smack.test.util.SmackTestSuite; +import org.jivesoftware.smack.util.PacketParserUtils; +import org.jivesoftware.smack.util.stringencoder.Base64; +import org.jivesoftware.smack.xml.XmlPullParser; +import org.jivesoftware.smackx.avatar.element.DataExtension; +import org.jivesoftware.smackx.avatar.provider.DataProvider; + +import org.junit.Assert; +import org.junit.Test; + +public class DataExtensionTest extends SmackTestSuite { + + // @formatter:off + String dataExtensionExample = "" + + "qANQR1DBwU4DX7jmYZnnfe32" + + ""; + // @formatter:on + + @Test + public void checkDataExtensionParse() throws Exception { + byte[] data = Base64.decode("qANQR1DBwU4DX7jmYZnnfe32"); + DataExtension dataExtension = new DataExtension(data); + Assert.assertEquals(dataExtensionExample, dataExtension.toXML().toString()); + + XmlPullParser parser = PacketParserUtils.getParserFor(dataExtensionExample); + DataExtension dataExtensionFromProvider = new DataProvider().parse(parser); + Assert.assertEquals(Base64.encodeToString(data), Base64.encodeToString(dataExtensionFromProvider.getData())); + } + +} diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/avatar/MetadataExtensionTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/avatar/MetadataExtensionTest.java new file mode 100644 index 000000000..a4016a040 --- /dev/null +++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/avatar/MetadataExtensionTest.java @@ -0,0 +1,208 @@ +/** + * + * Copyright 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. + * 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.avatar; + +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.jivesoftware.smack.util.PacketParserUtils; +import org.jivesoftware.smack.xml.XmlPullParser; +import org.jivesoftware.smackx.avatar.element.MetadataExtension; +import org.jivesoftware.smackx.avatar.provider.MetadataProvider; + +import org.junit.Assert; +import org.junit.Test; + +public class MetadataExtensionTest { + + private static final String metadataExtensionExample = "" + + "" + + ""; + + private static final String emptyMetadataExtensionExample = ""; + + private static final String metadataWithSeveralInfos = "" + + "" + + "" + + "" + + ""; + + private static final String metadataWithInfoAndPointers = "" + + "" + + "" + + "" + + "Ancapistan" + + "Kropotkin" + + "" + + "" + + "" + + "" + + "hard" + + "2" + + "" + + "" + + ""; + + @Test + public void checkMetadataExtensionParse() throws Exception { + String id = "357a8123a30844a3aa99861b6349264ba67a5694"; + URL url = new URL("http://avatars.example.org/happy.gif"); + long bytes = 23456; + String type = "image/gif"; + int pixelsHeight = 64; + int pixelsWidth = 128; + + MetadataInfo info = new MetadataInfo(id, url, bytes, type, pixelsHeight, pixelsWidth); + List infos = new ArrayList<>(); + infos.add(info); + + MetadataExtension metadataExtension = new MetadataExtension(infos); + Assert.assertEquals(metadataExtensionExample, metadataExtension.toXML().toString()); + + XmlPullParser parser = PacketParserUtils.getParserFor(metadataExtensionExample); + MetadataExtension metadataExtensionFromProvider = new MetadataProvider().parse(parser); + + Assert.assertEquals(id, metadataExtensionFromProvider.getInfoElements().get(0).getId()); + Assert.assertEquals(url, metadataExtensionFromProvider.getInfoElements().get(0).getUrl()); + Assert.assertEquals(bytes, metadataExtensionFromProvider.getInfoElements().get(0).getBytes().intValue()); + Assert.assertEquals(type, metadataExtensionFromProvider.getInfoElements().get(0).getType()); + Assert.assertEquals(pixelsHeight, metadataExtensionFromProvider.getInfoElements().get(0).getHeight().intValue()); + Assert.assertEquals(pixelsWidth, metadataExtensionFromProvider.getInfoElements().get(0).getWidth().intValue()); + } + + @Test + public void checkEmptyMetadataExtensionParse() throws Exception { + MetadataExtension metadataExtension = new MetadataExtension(null); + Assert.assertEquals(emptyMetadataExtensionExample, metadataExtension.toXML().toString()); + } + + @Test + public void checkSeveralInfosInMetadataExtension() throws Exception { + XmlPullParser parser = PacketParserUtils.getParserFor(metadataWithSeveralInfos); + MetadataExtension metadataExtensionFromProvider = new MetadataProvider().parse(parser); + + MetadataInfo info1 = metadataExtensionFromProvider.getInfoElements().get(0); + MetadataInfo info2 = metadataExtensionFromProvider.getInfoElements().get(1); + MetadataInfo info3 = metadataExtensionFromProvider.getInfoElements().get(2); + + Assert.assertEquals("111f4b3c50d7b0df729d299bc6f8e9ef9066971f", info1.getId()); + Assert.assertNull(info1.getUrl()); + Assert.assertEquals(12345, info1.getBytes().intValue()); + Assert.assertEquals("image/png", info1.getType()); + Assert.assertEquals(64, info1.getHeight().intValue()); + Assert.assertEquals(64, info1.getWidth().intValue()); + + Assert.assertEquals("e279f80c38f99c1e7e53e262b440993b2f7eea57", info2.getId()); + Assert.assertEquals(new URL("http://avatars.example.org/happy.png"), info2.getUrl()); + Assert.assertEquals(12345, info2.getBytes().intValue()); + Assert.assertEquals("image/png", info2.getType()); + Assert.assertEquals(64, info2.getHeight().intValue()); + Assert.assertEquals(128, info2.getWidth().intValue()); + + Assert.assertEquals("357a8123a30844a3aa99861b6349264ba67a5694", info3.getId()); + Assert.assertEquals(new URL("http://avatars.example.org/happy.gif"), info3.getUrl()); + Assert.assertEquals(23456, info3.getBytes().intValue()); + Assert.assertEquals("image/gif", info3.getType()); + Assert.assertEquals(64, info3.getHeight().intValue()); + Assert.assertEquals(64, info3.getWidth().intValue()); + } + + @Test + public void checkInfosAndPointersParse() throws Exception { + XmlPullParser parser = PacketParserUtils.getParserFor(metadataWithInfoAndPointers); + MetadataExtension metadataExtensionFromProvider = new MetadataProvider().parse(parser); + + MetadataInfo info = metadataExtensionFromProvider.getInfoElements().get(0); + Assert.assertEquals("111f4b3c50d7b0df729d299bc6f8e9ef9066971f", info.getId()); + Assert.assertNull(info.getUrl()); + Assert.assertEquals(12345, info.getBytes().intValue()); + Assert.assertEquals("image/png", info.getType()); + Assert.assertEquals(64, info.getHeight().intValue()); + Assert.assertEquals(64, info.getWidth().intValue()); + + MetadataPointer pointer1 = metadataExtensionFromProvider.getPointerElements().get(0); + Map fields1 = pointer1.getFields(); + Assert.assertEquals("http://example.com/virtualworlds", pointer1.getNamespace()); + Assert.assertEquals("Ancapistan", fields1.get("game")); + Assert.assertEquals("Kropotkin", fields1.get("character")); + + MetadataPointer pointer2 = metadataExtensionFromProvider.getPointerElements().get(1); + Map fields2 = pointer2.getFields(); + Assert.assertEquals("http://sample.com/game", pointer2.getNamespace()); + Assert.assertEquals("hard", fields2.get("level")); + Assert.assertEquals("2", fields2.get("players")); + } + + @Test + public void createMetadataExtensionWithInfoAndPointer() { + String id = "111f4b3c50d7b0df729d299bc6f8e9ef9066971f"; + long bytes = 12345; + String type = "image/png"; + int pixelsHeight = 64; + int pixelsWidth = 64; + MetadataInfo info = new MetadataInfo(id, null, bytes, type, pixelsHeight, pixelsWidth); + + HashMap fields1 = new HashMap<>(); + fields1.put("game", "Ancapistan"); + fields1.put("character", "Kropotkin"); + MetadataPointer pointer1 = new MetadataPointer("http://example.com/virtualworlds", fields1); + + HashMap fields2 = new HashMap<>(); + fields2.put("level", "hard"); + fields2.put("players", 2); + MetadataPointer pointer2 = new MetadataPointer("http://sample.com/game", fields2); + + List infos = new ArrayList<>(); + infos.add(info); + + List pointers = new ArrayList<>(); + pointers.add(pointer1); + pointers.add(pointer2); + + MetadataExtension metadataExtension = new MetadataExtension(infos, pointers); + Assert.assertEquals(metadataWithInfoAndPointers, metadataExtension.toXML().toString()); + } + +}