diff --git a/documentation/extensions/index.md b/documentation/extensions/index.md index 5043f940d..128fdf519 100644 --- a/documentation/extensions/index.md +++ b/documentation/extensions/index.md @@ -49,6 +49,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. | @@ -79,7 +80,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/smack-core/src/main/java/org/jivesoftware/smack/util/XmlStringBuilder.java b/smack-core/src/main/java/org/jivesoftware/smack/util/XmlStringBuilder.java index b9c7e5ca6..387bdd9eb 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; @@ -369,6 +370,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 62adda545..289dddf57 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 @@ -596,4 +596,15 @@ org.jivesoftware.smackx.usertune.provider.UserTuneProvider + + + data + urn:xmpp:avatar:data + org.jivesoftware.smackx.avatar.provider.DataProvider + + + metadata + urn:xmpp:avatar:metadata + org.jivesoftware.smackx.avatar.provider.MetadataProvider + 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()); + } + +}