fields;
+
+ /**
+ * Metadata Pointer constructor.
+ *
+ * The following example
+ *
+ * {@code
+ *
+ *
+ * Ancapistan
+ * Kropotkin
+ *
+ *
+ * }
+ *
+ * can be created by constructing the object like this:
+ *
+ * {@code
+ * Map fields = new HashMap<>();
+ * fields.add("game", "Ancapistan");
+ * fields.add("character", "Kropotkin");
+ * MetadataPointer pointer = new MetadataPointer("http://example.com/virtualworlds", fields);
+ * }
+ *
+ *
+ * @param namespace namespace of the child element of the metadata pointer.
+ * @param fields fields of the child element as key, value pairs.
+ */
+ public MetadataPointer(String namespace, Map fields) {
+ this.namespace = StringUtils.requireNotNullNorEmpty(namespace, "Namespace MUST NOT be null, nor empty.");
+ this.fields = fields;
+ }
+
+ /**
+ * Get the namespace of the pointers child element.
+ *
+ * @return the namespace
+ */
+ public String getNamespace() {
+ return namespace;
+ }
+
+ /**
+ * Get the fields of the pointers child element.
+ *
+ * @return the fields
+ */
+ public Map getFields() {
+ return fields;
+ }
+
+}
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/UserAvatarManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/UserAvatarManager.java
new file mode 100644
index 000000000..089157e4d
--- /dev/null
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/UserAvatarManager.java
@@ -0,0 +1,411 @@
+/**
+ *
+ * Copyright 2017 Fernando Ramirez, 2019 Paul Schaub
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.avatar;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.WeakHashMap;
+
+import org.jivesoftware.smack.Manager;
+import org.jivesoftware.smack.SmackException.NoResponseException;
+import org.jivesoftware.smack.SmackException.NotConnectedException;
+import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smack.XMPPException.XMPPErrorException;
+import org.jivesoftware.smack.packet.ExtensionElement;
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smack.util.SHA1;
+import org.jivesoftware.smack.util.stringencoder.Base64;
+import org.jivesoftware.smackx.avatar.element.DataExtension;
+import org.jivesoftware.smackx.avatar.element.MetadataExtension;
+import org.jivesoftware.smackx.avatar.listener.AvatarListener;
+import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
+import org.jivesoftware.smackx.pep.PepListener;
+import org.jivesoftware.smackx.pep.PepManager;
+import org.jivesoftware.smackx.pubsub.EventElement;
+import org.jivesoftware.smackx.pubsub.ItemsExtension;
+import org.jivesoftware.smackx.pubsub.LeafNode;
+import org.jivesoftware.smackx.pubsub.PayloadItem;
+import org.jivesoftware.smackx.pubsub.PubSubException;
+import org.jivesoftware.smackx.pubsub.PubSubManager;
+
+import org.jxmpp.jid.EntityBareJid;
+
+/**
+ * User Avatar manager class.
+ *
+ * @author Fernando Ramirez
+ * @author Paul Schaub
+ * @see XEP-0084: User
+ * Avatar
+ */
+public final class UserAvatarManager extends Manager {
+
+ public static final String DATA_NAMESPACE = "urn:xmpp:avatar:data";
+ public static final String METADATA_NAMESPACE = "urn:xmpp:avatar:metadata";
+ public static final String FEATURE_METADATA = METADATA_NAMESPACE + "+notify";
+
+ private static final Map INSTANCES = new WeakHashMap<>();
+
+ private final PepManager pepManager;
+ private final ServiceDiscoveryManager serviceDiscoveryManager;
+
+ private AvatarMetadataStore metadataStore;
+ private final Set avatarListeners = new HashSet<>();
+
+ /**
+ * Get the singleton instance of UserAvatarManager.
+ *
+ * @param connection {@link XMPPConnection}.
+ * @return the instance of UserAvatarManager
+ */
+ public static synchronized UserAvatarManager getInstanceFor(XMPPConnection connection) {
+ UserAvatarManager userAvatarManager = INSTANCES.get(connection);
+
+ if (userAvatarManager == null) {
+ userAvatarManager = new UserAvatarManager(connection);
+ INSTANCES.put(connection, userAvatarManager);
+ }
+
+ return userAvatarManager;
+ }
+
+ private UserAvatarManager(XMPPConnection connection) {
+ super(connection);
+ this.pepManager = PepManager.getInstanceFor(connection);
+ this.serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection);
+ }
+
+ /**
+ * Returns true if User Avatar publishing is supported by the server.
+ * In order to support User Avatars the server must have support for XEP-0163: Personal Eventing Protocol (PEP).
+ *
+ * @return true if User Avatar is supported by the server.
+ *
+ * @see XEP-0163: Personal Eventing Protocol
+ *
+ * @throws NoResponseException
+ * @throws XMPPErrorException
+ * @throws NotConnectedException
+ * @throws InterruptedException
+ */
+ public boolean isSupportedByServer()
+ throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
+ return pepManager.isSupported();
+ }
+
+ /**
+ * Announce support for User Avatars and start receiving avatar updates.
+ */
+ public void enable() {
+ pepManager.addPepListener(metadataExtensionListener);
+ serviceDiscoveryManager.addFeature(FEATURE_METADATA);
+ }
+
+ /**
+ * Stop receiving avatar updates.
+ */
+ public void disable() {
+ serviceDiscoveryManager.removeFeature(FEATURE_METADATA);
+ pepManager.addPepListener(metadataExtensionListener);
+ }
+
+ /**
+ * Set an {@link AvatarMetadataStore} which is used to store information about the local availability of avatar
+ * data.
+ * @param metadataStore metadata store
+ */
+ public void setAvatarMetadataStore(AvatarMetadataStore metadataStore) {
+ this.metadataStore = metadataStore;
+ }
+
+ /**
+ * Register an {@link AvatarListener} in order to be notified about incoming avatar metadata updates.
+ *
+ * @param listener listener
+ * @return true if the set of listeners did not already contain the listener
+ */
+ public synchronized boolean addAvatarListener(AvatarListener listener) {
+ return avatarListeners.add(listener);
+ }
+
+ /**
+ * Unregister an {@link AvatarListener} to stop being notified about incoming avatar metadata updates.
+ *
+ * @param listener listener
+ * @return true if the set of listeners contained the listener
+ */
+ public synchronized boolean removeAvatarListener(AvatarListener listener) {
+ return avatarListeners.remove(listener);
+ }
+
+ /**
+ * Get the data node.
+ * This node contains the avatar image data.
+ *
+ * @return the data node
+ * @throws NoResponseException
+ * @throws NotConnectedException
+ * @throws InterruptedException
+ * @throws XMPPErrorException
+ */
+ private LeafNode getOrCreateDataNode()
+ throws NoResponseException, NotConnectedException, InterruptedException, XMPPErrorException, PubSubException.NotALeafNodeException {
+ return pepManager.getPepPubSubManager().getOrCreateLeafNode(DATA_NAMESPACE);
+ }
+
+ /**
+ * Get the metadata node.
+ * This node contains lightweight metadata information about the data in the data node.
+ *
+ * @return the metadata node
+ * @throws NoResponseException
+ * @throws NotConnectedException
+ * @throws InterruptedException
+ * @throws XMPPErrorException
+ */
+ private LeafNode getOrCreateMetadataNode()
+ throws NoResponseException, NotConnectedException, InterruptedException, XMPPErrorException, PubSubException.NotALeafNodeException {
+ return pepManager.getPepPubSubManager().getOrCreateLeafNode(METADATA_NAMESPACE);
+ }
+
+ /**
+ * Publish a PNG Avatar and its metadata to PubSub.
+ *
+ * @param data
+ * @param height
+ * @param width
+ * @throws XMPPErrorException
+ * @throws PubSubException.NotALeafNodeException
+ * @throws NotConnectedException
+ * @throws InterruptedException
+ * @throws NoResponseException
+ */
+ public void publishAvatar(byte[] data, int height, int width)
+ throws XMPPErrorException, PubSubException.NotALeafNodeException, NotConnectedException,
+ InterruptedException, NoResponseException {
+ String id = publishAvatarData(data);
+ publishAvatarMetadata(id, data.length, "image/png", height, width);
+ }
+
+ /**
+ * Publish a PNG avatar and its metadata to PubSub.
+ *
+ * @param pngFile PNG File
+ * @param height height of the image
+ * @param width width of the image
+ *
+ * @throws IOException
+ * @throws XMPPErrorException
+ * @throws PubSubException.NotALeafNodeException
+ * @throws NotConnectedException
+ * @throws InterruptedException
+ * @throws NoResponseException
+ */
+ public void publishAvatar(File pngFile, int height, int width)
+ throws IOException, XMPPErrorException, PubSubException.NotALeafNodeException, NotConnectedException,
+ InterruptedException, NoResponseException {
+ try (ByteArrayOutputStream out = new ByteArrayOutputStream((int) pngFile.length());
+ InputStream in = new BufferedInputStream(new FileInputStream(pngFile))) {
+ byte[] buffer = new byte[4096];
+ int read;
+ while ((read = in.read(buffer)) != -1) {
+ out.write(buffer, 0, read);
+ }
+ byte[] bytes = out.toByteArray();
+ publishAvatar(bytes, height, width);
+ }
+ }
+
+ public byte[] fetchAvatarFromPubSub(EntityBareJid from, MetadataInfo metadataInfo)
+ throws InterruptedException, PubSubException.NotALeafNodeException, NoResponseException,
+ NotConnectedException, XMPPErrorException, PubSubException.NotAPubSubNodeException {
+ LeafNode dataNode = PubSubManager.getInstanceFor(connection(), from)
+ .getLeafNode(DATA_NAMESPACE);
+
+ List> dataItems = dataNode.getItems(1, metadataInfo.getId());
+ DataExtension extension = dataItems.get(0).getPayload();
+ if (metadataStore != null) {
+ metadataStore.setAvatarAvailable(from, metadataInfo.getId());
+ }
+ return extension.getData();
+ }
+
+ private String publishAvatarData(byte[] data)
+ throws NoResponseException, NotConnectedException, XMPPErrorException, InterruptedException, PubSubException.NotALeafNodeException {
+ String itemId = Base64.encodeToString(SHA1.bytes(data));
+ publishAvatarData(data, itemId);
+ return itemId;
+ }
+
+ private void publishAvatarData(byte[] data, String itemId)
+ throws NoResponseException, NotConnectedException, XMPPErrorException, InterruptedException, PubSubException.NotALeafNodeException {
+ DataExtension dataExtension = new DataExtension(data);
+ getOrCreateDataNode().publish(new PayloadItem<>(itemId, dataExtension));
+ }
+
+ /**
+ * Publish metadata about an avatar to the metadata node.
+ *
+ * @param itemId SHA-1 sum of the image of type image/png
+ * @param info info element containing metadata of the file
+ * @param pointers list of metadata pointer elements
+ *
+ * @throws NoResponseException
+ * @throws XMPPErrorException
+ * @throws NotConnectedException
+ * @throws InterruptedException
+ */
+ public void publishAvatarMetadata(String itemId, MetadataInfo info, List pointers)
+ throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, PubSubException.NotALeafNodeException {
+ publishAvatarMetadata(itemId, Collections.singletonList(info), pointers);
+ }
+
+ /**
+ * Publish avatar metadata.
+ *
+ * @param itemId SHA-1 sum of the avatar image representation of type image/png
+ * @param infos list of metadata elements
+ * @param pointers list of pointer elements
+ *
+ * @throws NoResponseException
+ * @throws XMPPErrorException
+ * @throws NotConnectedException
+ * @throws InterruptedException
+ */
+ public void publishAvatarMetadata(String itemId, List infos, List pointers)
+ throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, PubSubException.NotALeafNodeException {
+ MetadataExtension metadataExtension = new MetadataExtension(infos, pointers);
+ getOrCreateMetadataNode().publish(new PayloadItem<>(itemId, metadataExtension));
+
+ if (metadataStore == null) {
+ return;
+ }
+ // Mark our own avatar as locally available so that we don't get updates for it
+ metadataStore.setAvatarAvailable(connection().getUser().asEntityBareJidOrThrow(), itemId);
+ }
+
+ /**
+ * Publish metadata about an avatar available via HTTP.
+ * This method can be used together with HTTP File Upload as an alternative to PubSub for avatar publishing.
+ *
+ * @param itemId SHA-1 sum of the avatar image file.
+ * @param url HTTP(S) Url of the image file.
+ * @param bytes size of the file in bytes
+ * @param type content type of the file
+ * @param pixelsHeight height of the image file in pixels
+ * @param pixelsWidth width of the image file in pixels
+ *
+ * @throws NoResponseException
+ * @throws XMPPErrorException
+ * @throws NotConnectedException
+ * @throws InterruptedException
+ */
+ public void publishHttpAvatarMetadata(String itemId, URL url, long bytes, String type,
+ int pixelsHeight, int pixelsWidth)
+ throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, PubSubException.NotALeafNodeException {
+ MetadataInfo info = new MetadataInfo(itemId, url, bytes, type, pixelsHeight, pixelsWidth);
+ publishAvatarMetadata(itemId, info, null);
+ }
+
+ /**
+ * Publish avatar metadata with its size in pixels.
+ *
+ * @param itemId
+ * @param bytes
+ * @param type
+ * @param pixelsHeight
+ * @param pixelsWidth
+ * @throws NoResponseException
+ * @throws XMPPErrorException
+ * @throws NotConnectedException
+ * @throws InterruptedException
+ */
+ public void publishAvatarMetadata(String itemId, long bytes, String type, int pixelsHeight,
+ int pixelsWidth)
+ throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, PubSubException.NotALeafNodeException {
+ MetadataInfo info = new MetadataInfo(itemId, null, bytes, type, pixelsHeight, pixelsWidth);
+ publishAvatarMetadata(itemId, info, null);
+ }
+
+ /**
+ * Publish an empty metadata element to disable avatar publishing.
+ *
+ * @see ยง4.2 Metadata Element
+ *
+ * @throws NoResponseException
+ * @throws XMPPErrorException
+ * @throws NotConnectedException
+ * @throws InterruptedException
+ */
+ public void unpublishAvatar()
+ throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, PubSubException.NotALeafNodeException {
+ getOrCreateMetadataNode().publish(new PayloadItem<>(new MetadataExtension(null)));
+ }
+
+ private final PepListener metadataExtensionListener = new PepListener() {
+ @Override
+ public void eventReceived(EntityBareJid from, EventElement event, Message message) {
+ if (!MetadataExtension.NAMESPACE.equals(event.getNamespace())) {
+ // Totally not of interest for us.
+ return;
+ }
+
+ if (!MetadataExtension.ELEMENT.equals(event.getElementName())) {
+ return;
+ }
+
+ for (ExtensionElement items : event.getExtensions()) {
+ if (!(items instanceof ItemsExtension)) {
+ continue;
+ }
+
+ for (ExtensionElement item : ((ItemsExtension) items).getExtensions()) {
+ if (!(item instanceof PayloadItem>)) {
+ continue;
+ }
+
+ PayloadItem> payloadItem = (PayloadItem>) item;
+
+ if (!(payloadItem.getPayload() instanceof MetadataExtension)) {
+ continue;
+ }
+
+ MetadataExtension metadataExtension = (MetadataExtension) payloadItem.getPayload();
+ if (metadataStore != null && metadataStore.hasAvatarAvailable(from, ((PayloadItem>) item).getId())) {
+ // The metadata store implies that we have a local copy of the published image already. Skip.
+ continue;
+ }
+
+ for (AvatarListener listener : avatarListeners) {
+ listener.onAvatarUpdateReceived(from, metadataExtension);
+ }
+ }
+ }
+ }
+ };
+
+}
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/element/DataExtension.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/element/DataExtension.java
new file mode 100644
index 000000000..649e102d7
--- /dev/null
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/element/DataExtension.java
@@ -0,0 +1,102 @@
+/**
+ *
+ * Copyright 2017 Fernando Ramirez, 2019 Paul Schaub
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.avatar.element;
+
+import org.jivesoftware.smack.packet.ExtensionElement;
+import org.jivesoftware.smack.packet.XmlEnvironment;
+import org.jivesoftware.smack.util.StringUtils;
+import org.jivesoftware.smack.util.XmlStringBuilder;
+import org.jivesoftware.smack.util.stringencoder.Base64;
+import org.jivesoftware.smackx.avatar.UserAvatarManager;
+
+/**
+ * Data extension element class used to publish avatar image data via PubSub.
+ * Unlike the {@link MetadataExtension}, this class is dedicated to containing the avatar image data itself.
+ *
+ * @author Fernando Ramirez
+ * @see XEP-0084: User
+ * Avatar
+ */
+public class DataExtension implements ExtensionElement {
+
+ public static final String ELEMENT = "data";
+ public static final String NAMESPACE = UserAvatarManager.DATA_NAMESPACE;
+
+ private final byte[] data;
+ private String data_b64; // lazy initialized base64 encoded copy of data
+
+ /**
+ * Create a {@link DataExtension} from a byte array.
+ *
+ * @param data bytes of the image.
+ */
+ public DataExtension(byte[] data) {
+ this.data = data;
+ }
+
+ /**
+ * Create a {@link DataExtension} from a base64 encoded String.
+ *
+ * @param base64data bytes of the image as base64 string.
+ */
+ public DataExtension(String base64data) {
+ this.data_b64 = StringUtils.requireNotNullNorEmpty(base64data,
+ "Base64 String MUST NOT be null, nor empty.");
+ this.data = Base64.decode(base64data);
+ }
+
+ /**
+ * Get the bytes of the image.
+ *
+ * @return an immutable copy of the image data
+ */
+ public byte[] getData() {
+ return data.clone();
+ }
+
+ /**
+ * Get the image data encoded as a base64 String.
+ *
+ * @return the data as String
+ */
+ public String getDataAsString() {
+ if (data_b64 == null) {
+ data_b64 = Base64.encodeToString(data);
+ }
+ return data_b64;
+ }
+
+ @Override
+ public String getElementName() {
+ return ELEMENT;
+ }
+
+ @Override
+ public String getNamespace() {
+ return NAMESPACE;
+ }
+
+ @Override
+ public CharSequence toXML(XmlEnvironment xmlEnvironment) {
+ XmlStringBuilder xml = new XmlStringBuilder(this);
+ xml.rightAngleBracket();
+ xml.escape(this.getDataAsString());
+ xml.closeElement(this);
+ return xml;
+ }
+
+}
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/element/MetadataExtension.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/element/MetadataExtension.java
new file mode 100644
index 000000000..6f95d66f5
--- /dev/null
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/element/MetadataExtension.java
@@ -0,0 +1,173 @@
+/**
+ *
+ * Copyright 2017 Fernando Ramirez
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.avatar.element;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import org.jivesoftware.smack.packet.ExtensionElement;
+import org.jivesoftware.smack.packet.Stanza;
+import org.jivesoftware.smack.packet.XmlEnvironment;
+import org.jivesoftware.smack.util.XmlStringBuilder;
+import org.jivesoftware.smackx.avatar.MetadataInfo;
+import org.jivesoftware.smackx.avatar.MetadataPointer;
+import org.jivesoftware.smackx.avatar.UserAvatarManager;
+
+/**
+ * Metadata extension element class.
+ * This class contains metadata about published avatars.
+ *
+ * @author Fernando Ramirez
+ * @see XEP-0084: User
+ * Avatar
+ */
+public class MetadataExtension implements ExtensionElement {
+
+ public static final String ELEMENT = "metadata";
+ public static final String NAMESPACE = UserAvatarManager.METADATA_NAMESPACE;
+
+ private final List infos;
+ private final List pointers;
+
+ /**
+ * Metadata Extension constructor.
+ *
+ * @param infos
+ */
+ public MetadataExtension(List infos) {
+ this(infos, null);
+ }
+
+ /**
+ * Metadata Extension constructor.
+ *
+ * @param infos
+ * @param pointers
+ */
+ public MetadataExtension(List infos, List pointers) {
+ this.infos = infos;
+ this.pointers = pointers;
+ }
+
+ /**
+ * Get the info elements list.
+ *
+ * @return the info elements list
+ */
+ public List getInfoElements() {
+ return Collections.unmodifiableList(infos);
+ }
+
+ /**
+ * Get the pointer elements list.
+ *
+ * @return the pointer elements list
+ */
+ public List getPointerElements() {
+ return (pointers == null) ? null : Collections.unmodifiableList(pointers);
+ }
+
+ @Override
+ public String getElementName() {
+ return ELEMENT;
+ }
+
+ @Override
+ public String getNamespace() {
+ return NAMESPACE;
+ }
+
+ @Override
+ public CharSequence toXML(XmlEnvironment xmlEnvironment) {
+ XmlStringBuilder xml = new XmlStringBuilder(this);
+ appendInfoElements(xml);
+ appendPointerElements(xml);
+ closeElement(xml);
+ return xml;
+ }
+
+ private void appendInfoElements(XmlStringBuilder xml) {
+ if (infos != null) {
+ xml.rightAngleBracket();
+
+ for (MetadataInfo info : infos) {
+ xml.halfOpenElement("info");
+ xml.attribute("id", info.getId());
+ xml.attribute("bytes", info.getBytes().longValue());
+ xml.attribute("type", info.getType());
+ xml.optAttribute("url", info.getUrl());
+
+ if (info.getHeight().nativeRepresentation() > 0) {
+ xml.attribute("height", info.getHeight().nativeRepresentation());
+ }
+
+ if (info.getWidth().nativeRepresentation() > 0) {
+ xml.attribute("width", info.getWidth().nativeRepresentation());
+ }
+
+ xml.closeEmptyElement();
+ }
+ }
+ }
+
+ private void appendPointerElements(XmlStringBuilder xml) {
+ if (pointers != null) {
+
+ for (MetadataPointer pointer : pointers) {
+ xml.openElement("pointer");
+ xml.halfOpenElement("x");
+
+ String namespace = pointer.getNamespace();
+ if (namespace != null) {
+ xml.xmlnsAttribute(namespace);
+ }
+
+ xml.rightAngleBracket();
+
+ Map fields = pointer.getFields();
+ if (fields != null) {
+ for (Map.Entry pair : fields.entrySet()) {
+ xml.escapedElement(pair.getKey(), String.valueOf(pair.getValue()));
+ }
+ }
+
+ xml.closeElement("x");
+ xml.closeElement("pointer");
+ }
+
+ }
+ }
+
+ private void closeElement(XmlStringBuilder xml) {
+ if (infos != null || pointers != null) {
+ xml.closeElement(this);
+ } else {
+ xml.closeEmptyElement();
+ }
+ }
+
+ /**
+ * Return true, if this {@link MetadataExtension} is to be interpreted as Avatar unpublishing.
+ *
+ * @return true if unpublishing, false otherwise
+ */
+ public boolean isDisablingPublishing() {
+ return getInfoElements().isEmpty();
+ }
+
+}
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/element/package-info.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/element/package-info.java
new file mode 100644
index 000000000..b55f1aa91
--- /dev/null
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/element/package-info.java
@@ -0,0 +1,24 @@
+/**
+ *
+ * Copyright 2017 Fernando Ramirez
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * User Avatar elements.
+ *
+ * @author Fernando Ramirez
+ * @see XEP-0084: User
+ * Avatar
+ */
+package org.jivesoftware.smackx.avatar.element;
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/listener/AvatarListener.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/listener/AvatarListener.java
new file mode 100644
index 000000000..bd657a3e5
--- /dev/null
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/listener/AvatarListener.java
@@ -0,0 +1,31 @@
+/**
+ *
+ * Copyright 2019 Paul Schaub
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.avatar.listener;
+
+import org.jivesoftware.smackx.avatar.element.MetadataExtension;
+
+import org.jxmpp.jid.EntityBareJid;
+
+/**
+ * Listener that can notify the user about User Avatar updates.
+ *
+ * @author Paul Schaub
+ */
+public interface AvatarListener {
+
+ void onAvatarUpdateReceived(EntityBareJid user, MetadataExtension metadata);
+}
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/listener/package-info.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/listener/package-info.java
new file mode 100644
index 000000000..160328bb4
--- /dev/null
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/listener/package-info.java
@@ -0,0 +1,24 @@
+/**
+ *
+ * Copyright 2019 Paul Schaub
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Classes and interfaces of User Avatar.
+ *
+ * @author Paul Schaub
+ * @see XEP-0084: User
+ * Avatar
+ */
+package org.jivesoftware.smackx.avatar.listener;
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/package-info.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/package-info.java
new file mode 100644
index 000000000..67f3ef5c6
--- /dev/null
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/package-info.java
@@ -0,0 +1,24 @@
+/**
+ *
+ * Copyright 2017 Fernando Ramirez
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Classes and interfaces of User Avatar.
+ *
+ * @author Fernando Ramirez
+ * @see XEP-0084: User
+ * Avatar
+ */
+package org.jivesoftware.smackx.avatar;
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/provider/DataProvider.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/provider/DataProvider.java
new file mode 100644
index 000000000..69de5221d
--- /dev/null
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/provider/DataProvider.java
@@ -0,0 +1,44 @@
+/**
+ *
+ * Copyright 2017 Fernando Ramirez
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.avatar.provider;
+
+import java.io.IOException;
+
+import org.jivesoftware.smack.packet.XmlEnvironment;
+import org.jivesoftware.smack.provider.ExtensionElementProvider;
+import org.jivesoftware.smack.util.stringencoder.Base64;
+import org.jivesoftware.smack.xml.XmlPullParser;
+import org.jivesoftware.smack.xml.XmlPullParserException;
+import org.jivesoftware.smackx.avatar.element.DataExtension;
+
+/**
+ * User Avatar data provider class.
+ *
+ * @author Fernando Ramirez
+ * @see XEP-0084: User
+ * Avatar
+ */
+public class DataProvider extends ExtensionElementProvider {
+
+ @Override
+ public DataExtension parse(XmlPullParser parser, int initialDepth, XmlEnvironment environment)
+ throws IOException, XmlPullParserException {
+ byte[] data = Base64.decode(parser.nextText());
+ return new DataExtension(data);
+ }
+
+}
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/provider/MetadataProvider.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/provider/MetadataProvider.java
new file mode 100644
index 000000000..4490bb007
--- /dev/null
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/provider/MetadataProvider.java
@@ -0,0 +1,154 @@
+/**
+ *
+ * Copyright 2017 Fernando Ramirez, 2019 Paul Schaub
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.avatar.provider;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import org.jivesoftware.smack.packet.XmlEnvironment;
+import org.jivesoftware.smack.provider.ExtensionElementProvider;
+import org.jivesoftware.smack.xml.XmlPullParser;
+import org.jivesoftware.smack.xml.XmlPullParserException;
+import org.jivesoftware.smackx.avatar.MetadataInfo;
+import org.jivesoftware.smackx.avatar.MetadataPointer;
+import org.jivesoftware.smackx.avatar.element.MetadataExtension;
+
+/**
+ * User Avatar metadata provider class.
+ *
+ * @author Fernando Ramirez
+ * @author Paul Schaub
+ * @see XEP-0084: User
+ * Avatar
+ */
+public class MetadataProvider extends ExtensionElementProvider {
+
+ @Override
+ public MetadataExtension parse(XmlPullParser parser, int initialDepth, XmlEnvironment xmlEnvironment) throws IOException, XmlPullParserException {
+ List metadataInfos = null;
+ List pointers = null;
+
+ while (true) {
+ XmlPullParser.Event eventType = parser.next();
+
+ if (eventType == XmlPullParser.Event.START_ELEMENT) {
+
+ if (parser.getName().equals("info")) {
+ if (metadataInfos == null) {
+ metadataInfos = new ArrayList<>();
+ }
+
+ MetadataInfo info = parseInfo(parser);
+ if (info.getId() != null) {
+ metadataInfos.add(info);
+ }
+ }
+
+ if (parser.getName().equals("pointer")) {
+ if (pointers == null) {
+ pointers = new ArrayList<>();
+ }
+
+ pointers.add(parsePointer(parser));
+ }
+
+ } else if (eventType == XmlPullParser.Event.END_ELEMENT) {
+ if (parser.getDepth() == initialDepth) {
+ break;
+ }
+ }
+ }
+
+ return new MetadataExtension(metadataInfos, pointers);
+ }
+
+ private MetadataInfo parseInfo(XmlPullParser parser) throws XmlPullParserException {
+ String id;
+ URL url = null;
+ long bytes = 0;
+ String type;
+ int pixelsHeight = 0;
+ int pixelsWidth = 0;
+
+ id = parser.getAttributeValue("", "id");
+ type = parser.getAttributeValue("", "type");
+ String urlString = parser.getAttributeValue("", "url");
+ if (urlString != null && !urlString.isEmpty()) {
+ try {
+ url = new URL(urlString);
+ } catch (MalformedURLException e) {
+ throw new XmlPullParserException("Cannot parse URL '" + urlString + "'");
+ }
+ }
+
+ String bytesString = parser.getAttributeValue("", "bytes");
+ if (bytesString != null) {
+ bytes = Long.parseLong(bytesString);
+ }
+
+ String widthString = parser.getAttributeValue("", "width");
+ if (widthString != null) {
+ pixelsWidth = Integer.parseInt(widthString);
+ }
+
+ String heightString = parser.getAttributeValue("", "height");
+ if (heightString != null) {
+ pixelsHeight = Integer.parseInt(heightString);
+ }
+
+ try {
+ return new MetadataInfo(id, url, bytes, type, pixelsHeight, pixelsWidth);
+ } catch (IllegalArgumentException e) {
+ throw new XmlPullParserException(e);
+ }
+ }
+
+ private MetadataPointer parsePointer(XmlPullParser parser) throws XmlPullParserException, IOException {
+ int pointerDepth = parser.getDepth();
+ String namespace = null;
+ HashMap fields = null;
+
+ while (true) {
+ XmlPullParser.Event eventType2 = parser.next();
+
+ if (eventType2 == XmlPullParser.Event.START_ELEMENT) {
+ if (parser.getName().equals("x")) {
+ namespace = parser.getNamespace();
+ } else {
+ if (fields == null) {
+ fields = new HashMap<>();
+ }
+
+ String name = parser.getName();
+ Object value = parser.nextText();
+ fields.put(name, value);
+ }
+ } else if (eventType2 == XmlPullParser.Event.END_ELEMENT) {
+ if (parser.getDepth() == pointerDepth) {
+ break;
+ }
+ }
+ }
+
+ return new MetadataPointer(namespace, fields);
+ }
+
+}
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/provider/package-info.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/provider/package-info.java
new file mode 100644
index 000000000..38f659ac9
--- /dev/null
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/provider/package-info.java
@@ -0,0 +1,24 @@
+/**
+ *
+ * Copyright 2017 Fernando Ramirez
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * User Avatar providers.
+ *
+ * @author Fernando Ramirez
+ * @see XEP-0084: User
+ * Avatar
+ */
+package org.jivesoftware.smackx.avatar.provider;
diff --git a/smack-extensions/src/main/resources/org.jivesoftware.smack.extensions/extensions.providers b/smack-extensions/src/main/resources/org.jivesoftware.smack.extensions/extensions.providers
index 6d7ebc38c..92476171d 100644
--- a/smack-extensions/src/main/resources/org.jivesoftware.smack.extensions/extensions.providers
+++ b/smack-extensions/src/main/resources/org.jivesoftware.smack.extensions/extensions.providers
@@ -317,6 +317,18 @@
org.jivesoftware.smackx.geoloc.provider.GeoLocationProvider
+
+
+ data
+ urn:xmpp:avatar:data
+ org.jivesoftware.smackx.avatar.provider.DataProvider
+
+
+ metadata
+ urn:xmpp:avatar:metadata
+ org.jivesoftware.smackx.avatar.provider.MetadataProvider
+
+
active
diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/avatar/DataExtensionTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/avatar/DataExtensionTest.java
new file mode 100644
index 000000000..6fb74807b
--- /dev/null
+++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/avatar/DataExtensionTest.java
@@ -0,0 +1,48 @@
+/**
+ *
+ * Copyright 2017 Fernando Ramirez
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.avatar;
+
+import org.jivesoftware.smack.test.util.SmackTestSuite;
+import org.jivesoftware.smack.util.PacketParserUtils;
+import org.jivesoftware.smack.util.stringencoder.Base64;
+import org.jivesoftware.smack.xml.XmlPullParser;
+import org.jivesoftware.smackx.avatar.element.DataExtension;
+import org.jivesoftware.smackx.avatar.provider.DataProvider;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class DataExtensionTest extends SmackTestSuite {
+
+ // @formatter:off
+ String dataExtensionExample = ""
+ + "qANQR1DBwU4DX7jmYZnnfe32"
+ + "";
+ // @formatter:on
+
+ @Test
+ public void checkDataExtensionParse() throws Exception {
+ byte[] data = Base64.decode("qANQR1DBwU4DX7jmYZnnfe32");
+ DataExtension dataExtension = new DataExtension(data);
+ Assert.assertEquals(dataExtensionExample, dataExtension.toXML().toString());
+
+ XmlPullParser parser = PacketParserUtils.getParserFor(dataExtensionExample);
+ DataExtension dataExtensionFromProvider = new DataProvider().parse(parser);
+ Assert.assertEquals(Base64.encodeToString(data), Base64.encodeToString(dataExtensionFromProvider.getData()));
+ }
+
+}
diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/avatar/MetadataExtensionTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/avatar/MetadataExtensionTest.java
new file mode 100644
index 000000000..a4016a040
--- /dev/null
+++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/avatar/MetadataExtensionTest.java
@@ -0,0 +1,208 @@
+/**
+ *
+ * Copyright 2017 Fernando Ramirez
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.avatar;
+
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.jivesoftware.smack.util.PacketParserUtils;
+import org.jivesoftware.smack.xml.XmlPullParser;
+import org.jivesoftware.smackx.avatar.element.MetadataExtension;
+import org.jivesoftware.smackx.avatar.provider.MetadataProvider;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class MetadataExtensionTest {
+
+ private static final String metadataExtensionExample = ""
+ + ""
+ + "";
+
+ private static final String emptyMetadataExtensionExample = "";
+
+ private static final String metadataWithSeveralInfos = ""
+ + ""
+ + ""
+ + ""
+ + "";
+
+ private static final String metadataWithInfoAndPointers = ""
+ + ""
+ + ""
+ + ""
+ + "Ancapistan"
+ + "Kropotkin"
+ + ""
+ + ""
+ + ""
+ + ""
+ + "hard"
+ + "2"
+ + ""
+ + ""
+ + "";
+
+ @Test
+ public void checkMetadataExtensionParse() throws Exception {
+ String id = "357a8123a30844a3aa99861b6349264ba67a5694";
+ URL url = new URL("http://avatars.example.org/happy.gif");
+ long bytes = 23456;
+ String type = "image/gif";
+ int pixelsHeight = 64;
+ int pixelsWidth = 128;
+
+ MetadataInfo info = new MetadataInfo(id, url, bytes, type, pixelsHeight, pixelsWidth);
+ List infos = new ArrayList<>();
+ infos.add(info);
+
+ MetadataExtension metadataExtension = new MetadataExtension(infos);
+ Assert.assertEquals(metadataExtensionExample, metadataExtension.toXML().toString());
+
+ XmlPullParser parser = PacketParserUtils.getParserFor(metadataExtensionExample);
+ MetadataExtension metadataExtensionFromProvider = new MetadataProvider().parse(parser);
+
+ Assert.assertEquals(id, metadataExtensionFromProvider.getInfoElements().get(0).getId());
+ Assert.assertEquals(url, metadataExtensionFromProvider.getInfoElements().get(0).getUrl());
+ Assert.assertEquals(bytes, metadataExtensionFromProvider.getInfoElements().get(0).getBytes().intValue());
+ Assert.assertEquals(type, metadataExtensionFromProvider.getInfoElements().get(0).getType());
+ Assert.assertEquals(pixelsHeight, metadataExtensionFromProvider.getInfoElements().get(0).getHeight().intValue());
+ Assert.assertEquals(pixelsWidth, metadataExtensionFromProvider.getInfoElements().get(0).getWidth().intValue());
+ }
+
+ @Test
+ public void checkEmptyMetadataExtensionParse() throws Exception {
+ MetadataExtension metadataExtension = new MetadataExtension(null);
+ Assert.assertEquals(emptyMetadataExtensionExample, metadataExtension.toXML().toString());
+ }
+
+ @Test
+ public void checkSeveralInfosInMetadataExtension() throws Exception {
+ XmlPullParser parser = PacketParserUtils.getParserFor(metadataWithSeveralInfos);
+ MetadataExtension metadataExtensionFromProvider = new MetadataProvider().parse(parser);
+
+ MetadataInfo info1 = metadataExtensionFromProvider.getInfoElements().get(0);
+ MetadataInfo info2 = metadataExtensionFromProvider.getInfoElements().get(1);
+ MetadataInfo info3 = metadataExtensionFromProvider.getInfoElements().get(2);
+
+ Assert.assertEquals("111f4b3c50d7b0df729d299bc6f8e9ef9066971f", info1.getId());
+ Assert.assertNull(info1.getUrl());
+ Assert.assertEquals(12345, info1.getBytes().intValue());
+ Assert.assertEquals("image/png", info1.getType());
+ Assert.assertEquals(64, info1.getHeight().intValue());
+ Assert.assertEquals(64, info1.getWidth().intValue());
+
+ Assert.assertEquals("e279f80c38f99c1e7e53e262b440993b2f7eea57", info2.getId());
+ Assert.assertEquals(new URL("http://avatars.example.org/happy.png"), info2.getUrl());
+ Assert.assertEquals(12345, info2.getBytes().intValue());
+ Assert.assertEquals("image/png", info2.getType());
+ Assert.assertEquals(64, info2.getHeight().intValue());
+ Assert.assertEquals(128, info2.getWidth().intValue());
+
+ Assert.assertEquals("357a8123a30844a3aa99861b6349264ba67a5694", info3.getId());
+ Assert.assertEquals(new URL("http://avatars.example.org/happy.gif"), info3.getUrl());
+ Assert.assertEquals(23456, info3.getBytes().intValue());
+ Assert.assertEquals("image/gif", info3.getType());
+ Assert.assertEquals(64, info3.getHeight().intValue());
+ Assert.assertEquals(64, info3.getWidth().intValue());
+ }
+
+ @Test
+ public void checkInfosAndPointersParse() throws Exception {
+ XmlPullParser parser = PacketParserUtils.getParserFor(metadataWithInfoAndPointers);
+ MetadataExtension metadataExtensionFromProvider = new MetadataProvider().parse(parser);
+
+ MetadataInfo info = metadataExtensionFromProvider.getInfoElements().get(0);
+ Assert.assertEquals("111f4b3c50d7b0df729d299bc6f8e9ef9066971f", info.getId());
+ Assert.assertNull(info.getUrl());
+ Assert.assertEquals(12345, info.getBytes().intValue());
+ Assert.assertEquals("image/png", info.getType());
+ Assert.assertEquals(64, info.getHeight().intValue());
+ Assert.assertEquals(64, info.getWidth().intValue());
+
+ MetadataPointer pointer1 = metadataExtensionFromProvider.getPointerElements().get(0);
+ Map fields1 = pointer1.getFields();
+ Assert.assertEquals("http://example.com/virtualworlds", pointer1.getNamespace());
+ Assert.assertEquals("Ancapistan", fields1.get("game"));
+ Assert.assertEquals("Kropotkin", fields1.get("character"));
+
+ MetadataPointer pointer2 = metadataExtensionFromProvider.getPointerElements().get(1);
+ Map fields2 = pointer2.getFields();
+ Assert.assertEquals("http://sample.com/game", pointer2.getNamespace());
+ Assert.assertEquals("hard", fields2.get("level"));
+ Assert.assertEquals("2", fields2.get("players"));
+ }
+
+ @Test
+ public void createMetadataExtensionWithInfoAndPointer() {
+ String id = "111f4b3c50d7b0df729d299bc6f8e9ef9066971f";
+ long bytes = 12345;
+ String type = "image/png";
+ int pixelsHeight = 64;
+ int pixelsWidth = 64;
+ MetadataInfo info = new MetadataInfo(id, null, bytes, type, pixelsHeight, pixelsWidth);
+
+ HashMap fields1 = new HashMap<>();
+ fields1.put("game", "Ancapistan");
+ fields1.put("character", "Kropotkin");
+ MetadataPointer pointer1 = new MetadataPointer("http://example.com/virtualworlds", fields1);
+
+ HashMap fields2 = new HashMap<>();
+ fields2.put("level", "hard");
+ fields2.put("players", 2);
+ MetadataPointer pointer2 = new MetadataPointer("http://sample.com/game", fields2);
+
+ List infos = new ArrayList<>();
+ infos.add(info);
+
+ List pointers = new ArrayList<>();
+ pointers.add(pointer1);
+ pointers.add(pointer2);
+
+ MetadataExtension metadataExtension = new MetadataExtension(infos, pointers);
+ Assert.assertEquals(metadataWithInfoAndPointers, metadataExtension.toXML().toString());
+ }
+
+}