1
0
Fork 0
mirror of https://codeberg.org/Mercury-IM/Smack synced 2024-11-23 23:02:05 +01:00

XEP-0084: User Avatars

Co-authored-by: vanitasvitae <vanitasvitae@fsfe.org>
This commit is contained in:
Paul Schaub 2019-08-31 14:17:13 +02:00
parent f61ecb65e7
commit f5ac8246f6
22 changed files with 1806 additions and 1 deletions

View file

@ -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. | | 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. | | 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. | | 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. | | 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. | | [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. | | 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. | | [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 | | [Jive Properties](properties.md) | n/a | n/a | TODO |
Experimental Smack Extensions and currently supported XEPs of smack-experimental Experimental Smack Extensions and currently supported XEPs of smack-experimental
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View file

@ -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

172
gradlew vendored Executable file
View file

@ -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" "$@"

84
gradlew.bat vendored Normal file
View file

@ -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

View file

@ -19,6 +19,7 @@ package org.jivesoftware.smack.util;
import java.io.IOException; import java.io.IOException;
import java.io.Writer; import java.io.Writer;
import java.util.ArrayList; import java.util.ArrayList;
import java.net.URL;
import java.util.Collection; import java.util.Collection;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
@ -358,6 +359,13 @@ public class XmlStringBuilder implements Appendable, CharSequence, Element {
return this; 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}. * Add the given attribute if {@code value => 0}.
* *

View file

@ -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);
}

View file

@ -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 <a href="http://xmpp.org/extensions/xep-0084.html">XEP-0084: User
* Avatar</a>
*/
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;
}
}

View file

@ -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 <a href="https://xmpp.org/extensions/xep-0084.html">XEP-0084: User Avatar</a>
*/
public class MetadataPointer {
private final String namespace;
private final Map<String, Object> fields;
/**
* Metadata Pointer constructor.
*
* The following example
* <pre>
* {@code
* <pointer>
* <x xmlns='http://example.com/virtualworlds'>
* <game>Ancapistan</game>
* <character>Kropotkin</character>
* </x>
* </pointer>
* }
* </pre>
* can be created by constructing the object like this:
* <pre>
* {@code
* Map fields = new HashMap<>();
* fields.add("game", "Ancapistan");
* fields.add("character", "Kropotkin");
* MetadataPointer pointer = new MetadataPointer("http://example.com/virtualworlds", fields);
* }
* </pre>
*
* @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<String, Object> 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<String, Object> getFields() {
return fields;
}
}

View file

@ -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 <a href="http://xmpp.org/extensions/xep-0084.html">XEP-0084: User
* Avatar</a>
*/
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<XMPPConnection, UserAvatarManager> INSTANCES = new WeakHashMap<>();
private final PepManager pepManager;
private final ServiceDiscoveryManager serviceDiscoveryManager;
private AvatarMetadataStore metadataStore;
private final Set<AvatarListener> 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 <a href="https://xmpp.org/extensions/xep-0163.html">XEP-0163: Personal Eventing Protocol</a>
*
* @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<PayloadItem<DataExtension>> 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<MetadataPointer> 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<MetadataInfo> infos, List<MetadataPointer> 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 <a href="https://xmpp.org/extensions/xep-0084.html#proto-meta">§4.2 Metadata Element</a>
*
* @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);
}
}
}
}
};
}

View file

@ -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 <a href="http://xmpp.org/extensions/xep-0084.html">XEP-0084: User
* Avatar</a>
*/
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;
}
}

View file

@ -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 <a href="https://xmpp.org/extensions/xep-0084.html">XEP-0084: User
* Avatar</a>
*/
public class MetadataExtension implements ExtensionElement {
public static final String ELEMENT = "metadata";
public static final String NAMESPACE = UserAvatarManager.METADATA_NAMESPACE;
private final List<MetadataInfo> infos;
private final List<MetadataPointer> pointers;
/**
* Metadata Extension constructor.
*
* @param infos
*/
public MetadataExtension(List<MetadataInfo> infos) {
this(infos, null);
}
/**
* Metadata Extension constructor.
*
* @param infos
* @param pointers
*/
public MetadataExtension(List<MetadataInfo> infos, List<MetadataPointer> pointers) {
this.infos = infos;
this.pointers = pointers;
}
/**
* Get the info elements list.
*
* @return the info elements list
*/
public List<MetadataInfo> getInfoElements() {
return Collections.unmodifiableList(infos);
}
/**
* Get the pointer elements list.
*
* @return the pointer elements list
*/
public List<MetadataPointer> 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<String, Object> fields = pointer.getFields();
if (fields != null) {
for (Map.Entry<String, Object> 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();
}
}

View file

@ -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 <a href="http://xmpp.org/extensions/xep-0084.html">XEP-0084: User
* Avatar</a>
*/
package org.jivesoftware.smackx.avatar.element;

View file

@ -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);
}

View file

@ -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 <a href="http://xmpp.org/extensions/xep-0084.html">XEP-0084: User
* Avatar</a>
*/
package org.jivesoftware.smackx.avatar.listener;

View file

@ -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 <a href="http://xmpp.org/extensions/xep-0084.html">XEP-0084: User
* Avatar</a>
*/
package org.jivesoftware.smackx.avatar;

View file

@ -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 <a href="http://xmpp.org/extensions/xep-0084.html">XEP-0084: User
* Avatar</a>
*/
public class DataProvider extends ExtensionElementProvider<DataExtension> {
@Override
public DataExtension parse(XmlPullParser parser, int initialDepth, XmlEnvironment environment)
throws IOException, XmlPullParserException {
byte[] data = Base64.decode(parser.nextText());
return new DataExtension(data);
}
}

View file

@ -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 <a href="http://xmpp.org/extensions/xep-0084.html">XEP-0084: User
* Avatar</a>
*/
public class MetadataProvider extends ExtensionElementProvider<MetadataExtension> {
@Override
public MetadataExtension parse(XmlPullParser parser, int initialDepth, XmlEnvironment xmlEnvironment) throws IOException, XmlPullParserException {
List<MetadataInfo> metadataInfos = null;
List<MetadataPointer> 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<String, Object> 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);
}
}

View file

@ -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 <a href="http://xmpp.org/extensions/xep-0084.html">XEP-0084: User
* Avatar</a>
*/
package org.jivesoftware.smackx.avatar.provider;

View file

@ -317,6 +317,18 @@
<className>org.jivesoftware.smackx.geoloc.provider.GeoLocationProvider</className> <className>org.jivesoftware.smackx.geoloc.provider.GeoLocationProvider</className>
</extensionProvider> </extensionProvider>
<!-- XEP-0084: User Avatar -->
<extensionProvider>
<elementName>data</elementName>
<namespace>urn:xmpp:avatar:data</namespace>
<className>org.jivesoftware.smackx.avatar.provider.DataProvider</className>
</extensionProvider>
<extensionProvider>
<elementName>metadata</elementName>
<namespace>urn:xmpp:avatar:metadata</namespace>
<className>org.jivesoftware.smackx.avatar.provider.MetadataProvider</className>
</extensionProvider>
<!-- XEP-0085: Chat State Notifications --> <!-- XEP-0085: Chat State Notifications -->
<extensionProvider> <extensionProvider>
<elementName>active</elementName> <elementName>active</elementName>

View file

@ -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 = "<data xmlns='urn:xmpp:avatar:data'>"
+ "qANQR1DBwU4DX7jmYZnnfe32"
+ "</data>";
// @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()));
}
}

View file

@ -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 = "<metadata xmlns='urn:xmpp:avatar:metadata'>"
+ "<info "
+ "id='357a8123a30844a3aa99861b6349264ba67a5694' "
+ "bytes='23456' "
+ "type='image/gif' "
+ "url='http://avatars.example.org/happy.gif' "
+ "height='64' "
+ "width='128'/>"
+ "</metadata>";
private static final String emptyMetadataExtensionExample = "<metadata xmlns='urn:xmpp:avatar:metadata'/>";
private static final String metadataWithSeveralInfos = "<metadata xmlns='urn:xmpp:avatar:metadata'>"
+ "<info bytes='12345'"
+ " height='64'"
+ " id='111f4b3c50d7b0df729d299bc6f8e9ef9066971f'"
+ " type='image/png'"
+ " width='64'/>"
+ "<info bytes='12345'"
+ " height='64'"
+ " id='e279f80c38f99c1e7e53e262b440993b2f7eea57'"
+ " type='image/png'"
+ " url='http://avatars.example.org/happy.png'"
+ " width='128'/>"
+ "<info bytes='23456'"
+ " height='64'"
+ " id='357a8123a30844a3aa99861b6349264ba67a5694'"
+ " type='image/gif'"
+ " url='http://avatars.example.org/happy.gif'"
+ " width='64'/>"
+ "</metadata>";
private static final String metadataWithInfoAndPointers = "<metadata xmlns='urn:xmpp:avatar:metadata'>"
+ "<info"
+ " id='111f4b3c50d7b0df729d299bc6f8e9ef9066971f'"
+ " bytes='12345'"
+ " type='image/png'"
+ " height='64'"
+ " width='64'/>"
+ "<pointer>"
+ "<x xmlns='http://example.com/virtualworlds'>"
+ "<game>Ancapistan</game>"
+ "<character>Kropotkin</character>"
+ "</x>"
+ "</pointer>"
+ "<pointer>"
+ "<x xmlns='http://sample.com/game'>"
+ "<level>hard</level>"
+ "<players>2</players>"
+ "</x>"
+ "</pointer>"
+ "</metadata>";
@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<MetadataInfo> 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<String, Object> 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<String, Object> 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<String, Object> fields1 = new HashMap<>();
fields1.put("game", "Ancapistan");
fields1.put("character", "Kropotkin");
MetadataPointer pointer1 = new MetadataPointer("http://example.com/virtualworlds", fields1);
HashMap<String, Object> fields2 = new HashMap<>();
fields2.put("level", "hard");
fields2.put("players", 2);
MetadataPointer pointer2 = new MetadataPointer("http://sample.com/game", fields2);
List<MetadataInfo> infos = new ArrayList<>();
infos.add(info);
List<MetadataPointer> pointers = new ArrayList<>();
pointers.add(pointer1);
pointers.add(pointer2);
MetadataExtension metadataExtension = new MetadataExtension(infos, pointers);
Assert.assertEquals(metadataWithInfoAndPointers, metadataExtension.toXML().toString());
}
}