Add UserAvatarException

This commit is contained in:
Paul Schaub 2019-08-31 15:53:15 +02:00
parent cee80edb13
commit 2b4689b3a2
3 changed files with 227 additions and 104 deletions

View File

@ -0,0 +1,66 @@
/**
*
* 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;
public class UserAvatarException extends AssertionError {
private static final long serialVersionUID = 1L;
private UserAvatarException(String message) {
super(message);
}
/**
* Exception that gets thrown, when the {@link org.jivesoftware.smackx.avatar.element.DataExtension DataExtensions}
* byte count does not match the {@link org.jivesoftware.smackx.avatar.element.MetadataExtension MetadataExtensions}
* 'bytes' value.
*/
public static class AvatarMetadataMismatchException extends UserAvatarException {
private static final long serialVersionUID = 1L;
public AvatarMetadataMismatchException(String message) {
super(message);
}
}
/**
* Exception that gets thrown when the user tries to publish a {@link org.jivesoftware.smackx.avatar.element.MetadataExtension}
* that is missing a required {@link MetadataInfo} for an image of type {@link UserAvatarManager#TYPE_PNG}.
*/
public static class AvatarMetadataMissingPNGInfoException extends UserAvatarException {
private static final long serialVersionUID = 1L;
public AvatarMetadataMissingPNGInfoException(String message) {
super(message);
}
}
/**
* Exception that gets thrown when the user tries to fetch a {@link org.jivesoftware.smackx.avatar.element.DataExtension}
* from PubSub using a {@link MetadataInfo} that does point to a HTTP resource.
*/
public static class NotAPubSubAvatarInfoElementException extends UserAvatarException {
private static final long serialVersionUID = 1L;
public NotAPubSubAvatarInfoElementException(String message) {
super(message);
}
}
}

View File

@ -70,6 +70,10 @@ public final class UserAvatarManager extends Manager {
private static final Map<XMPPConnection, UserAvatarManager> INSTANCES = new WeakHashMap<>(); private static final Map<XMPPConnection, UserAvatarManager> INSTANCES = new WeakHashMap<>();
public static final String TYPE_PNG = "image/png";
public static final String TYPE_GIF = "image/gif";
public static final String TYPE_JPEG = "image/jpeg";
private final PepManager pepManager; private final PepManager pepManager;
private final ServiceDiscoveryManager serviceDiscoveryManager; private final ServiceDiscoveryManager serviceDiscoveryManager;
@ -107,10 +111,10 @@ public final class UserAvatarManager extends Manager {
* *
* @see <a href="https://xmpp.org/extensions/xep-0163.html">XEP-0163: Personal Eventing Protocol</a> * @see <a href="https://xmpp.org/extensions/xep-0163.html">XEP-0163: Personal Eventing Protocol</a>
* *
* @throws NoResponseException * @throws NoResponseException if the server does not respond
* @throws XMPPErrorException * @throws XMPPErrorException if a protocol level error occurs
* @throws NotConnectedException * @throws NotConnectedException if the connection is not connected
* @throws InterruptedException * @throws InterruptedException if the thread is interrupted
*/ */
public boolean isSupportedByServer() public boolean isSupportedByServer()
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
@ -162,31 +166,11 @@ public final class UserAvatarManager extends Manager {
return avatarListeners.remove(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() private LeafNode getOrCreateDataNode()
throws NoResponseException, NotConnectedException, InterruptedException, XMPPErrorException, PubSubException.NotALeafNodeException { throws NoResponseException, NotConnectedException, InterruptedException, XMPPErrorException, PubSubException.NotALeafNodeException {
return pepManager.getPepPubSubManager().getOrCreateLeafNode(DATA_NAMESPACE); 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() private LeafNode getOrCreateMetadataNode()
throws NoResponseException, NotConnectedException, InterruptedException, XMPPErrorException, PubSubException.NotALeafNodeException { throws NoResponseException, NotConnectedException, InterruptedException, XMPPErrorException, PubSubException.NotALeafNodeException {
return pepManager.getPepPubSubManager().getOrCreateLeafNode(METADATA_NAMESPACE); return pepManager.getPepPubSubManager().getOrCreateLeafNode(METADATA_NAMESPACE);
@ -195,20 +179,22 @@ public final class UserAvatarManager extends Manager {
/** /**
* Publish a PNG Avatar and its metadata to PubSub. * Publish a PNG Avatar and its metadata to PubSub.
* *
* @param data * @param data raw bytes of the avatar
* @param height * @param height height of the image in pixels
* @param width * @param width width of the image in pixels
* @throws XMPPErrorException *
* @throws PubSubException.NotALeafNodeException * @throws XMPPErrorException if a protocol level error occurs
* @throws NotConnectedException * @throws PubSubException.NotALeafNodeException if either the metadata node or the data node is not a
* @throws InterruptedException * {@link LeafNode}
* @throws NoResponseException * @throws NotConnectedException if the connection is not connected
* @throws InterruptedException if the thread is interrupted
* @throws NoResponseException if the server does not respond
*/ */
public void publishAvatar(byte[] data, int height, int width) public void publishPNGAvatar(byte[] data, int height, int width)
throws XMPPErrorException, PubSubException.NotALeafNodeException, NotConnectedException, throws XMPPErrorException, PubSubException.NotALeafNodeException, NotConnectedException,
InterruptedException, NoResponseException { InterruptedException, NoResponseException {
String id = publishAvatarData(data); String id = publishPNGAvatarData(data);
publishAvatarMetadata(id, data.length, "image/png", height, width); publishPNGAvatarMetadata(id, data.length, height, width);
} }
/** /**
@ -218,14 +204,15 @@ public final class UserAvatarManager extends Manager {
* @param height height of the image * @param height height of the image
* @param width width of the image * @param width width of the image
* *
* @throws IOException * @throws IOException if an {@link IOException} occurs while reading the file
* @throws XMPPErrorException * @throws XMPPErrorException if a protocol level error occurs
* @throws PubSubException.NotALeafNodeException * @throws PubSubException.NotALeafNodeException if either the metadata node or the data node is not a valid
* @throws NotConnectedException * {@link LeafNode}
* @throws InterruptedException * @throws NotConnectedException if the connection is not connected
* @throws NoResponseException * @throws InterruptedException if the thread is interrupted
* @throws NoResponseException if the server does not respond
*/ */
public void publishAvatar(File pngFile, int height, int width) public void publishPNGAvatar(File pngFile, int height, int width)
throws IOException, XMPPErrorException, PubSubException.NotALeafNodeException, NotConnectedException, throws IOException, XMPPErrorException, PubSubException.NotALeafNodeException, NotConnectedException,
InterruptedException, NoResponseException { InterruptedException, NoResponseException {
try (ByteArrayOutputStream out = new ByteArrayOutputStream((int) pngFile.length()); try (ByteArrayOutputStream out = new ByteArrayOutputStream((int) pngFile.length());
@ -236,68 +223,139 @@ public final class UserAvatarManager extends Manager {
out.write(buffer, 0, read); out.write(buffer, 0, read);
} }
byte[] bytes = out.toByteArray(); byte[] bytes = out.toByteArray();
publishAvatar(bytes, height, width); publishPNGAvatar(bytes, height, width);
} }
} }
/**
* Fetch a published user avatar from their PubSub service.
*
* @param from {@link EntityBareJid} of the avatars owner
* @param metadataInfo {@link MetadataInfo} of the avatar that shall be fetched
*
* @return bytes of the avatar
*
* @throws InterruptedException if the thread gets interrupted
* @throws PubSubException.NotALeafNodeException if the data node is not a {@link LeafNode}
* @throws NoResponseException if the server does not respond
* @throws NotConnectedException if the connection is not connected
* @throws XMPPErrorException if a protocol level error occurs
* @throws PubSubException.NotAPubSubNodeException if the data node is not a valid PubSub node
* @throws UserAvatarException.AvatarMetadataMismatchException if the data in the data node does not match whats
* promised by the {@link MetadataInfo} element
* @throws UserAvatarException.NotAPubSubAvatarInfoElementException if the user tries to fetch the avatar using an
* info element that points to a HTTP resource
*/
public byte[] fetchAvatarFromPubSub(EntityBareJid from, MetadataInfo metadataInfo) public byte[] fetchAvatarFromPubSub(EntityBareJid from, MetadataInfo metadataInfo)
throws InterruptedException, PubSubException.NotALeafNodeException, NoResponseException, throws InterruptedException, PubSubException.NotALeafNodeException, NoResponseException,
NotConnectedException, XMPPErrorException, PubSubException.NotAPubSubNodeException { NotConnectedException, XMPPErrorException, PubSubException.NotAPubSubNodeException,
UserAvatarException.AvatarMetadataMismatchException {
if (metadataInfo.getUrl() != null) {
throw new UserAvatarException.NotAPubSubAvatarInfoElementException("Provided MetadataInfo element points to " +
"a HTTP resource, not to a PubSub item.");
}
LeafNode dataNode = PubSubManager.getInstanceFor(connection(), from) LeafNode dataNode = PubSubManager.getInstanceFor(connection(), from)
.getLeafNode(DATA_NAMESPACE); .getLeafNode(DATA_NAMESPACE);
List<PayloadItem<DataExtension>> dataItems = dataNode.getItems(1, metadataInfo.getId()); List<PayloadItem<DataExtension>> dataItems = dataNode.getItems(1, metadataInfo.getId());
DataExtension extension = dataItems.get(0).getPayload(); DataExtension data = dataItems.get(0).getPayload();
if (data.getData().length != metadataInfo.getBytes().intValue()) {
throw new UserAvatarException.AvatarMetadataMismatchException("Avatar Data with itemId '" + metadataInfo.getId() +
"' of " + from.asUnescapedString() + " does not match the Metadata (metadata promises " +
metadataInfo.getBytes().intValue() + " bytes, data contains " + data.getData().length + " bytes)");
}
if (metadataStore != null) { if (metadataStore != null) {
metadataStore.setAvatarAvailable(from, metadataInfo.getId()); metadataStore.setAvatarAvailable(from, metadataInfo.getId());
} }
return extension.getData(); return data.getData();
} }
private String publishAvatarData(byte[] data) private String publishPNGAvatarData(byte[] data)
throws NoResponseException, NotConnectedException, XMPPErrorException, InterruptedException, PubSubException.NotALeafNodeException { throws NoResponseException, NotConnectedException, XMPPErrorException, InterruptedException, PubSubException.NotALeafNodeException {
String itemId = Base64.encodeToString(SHA1.bytes(data)); String itemId = Base64.encodeToString(SHA1.bytes(data));
publishAvatarData(data, itemId); publishAvatarData(data, itemId);
return itemId; return itemId;
} }
private void publishAvatarData(byte[] data, String itemId) /**
* Publish some avatar image data to PubSub.
* Note, that if the image is an image of type {@link #TYPE_PNG}, the itemId MUST be the SHA-1 sum of that image.
* If however the image is not of type {@link #TYPE_PNG}, the itemId MUST be the SHA-1 sum of the PNG encoded
* representation of this image (an avatar can be published in several image formats, but at least one of them
* must be of type {@link #TYPE_PNG}).
*
* @param data raw bytes of the image
* @param itemId SHA-1 sum of the PNG encoded representation of this image.
*
* @throws NoResponseException if the server does not respond
* @throws NotConnectedException if the connection is not connected
* @throws XMPPErrorException if a protocol level error occurs
* @throws InterruptedException if the thread is interrupted
* @throws PubSubException.NotALeafNodeException if the data node is not a {@link LeafNode}.
*/
public void publishAvatarData(byte[] data, String itemId)
throws NoResponseException, NotConnectedException, XMPPErrorException, InterruptedException, PubSubException.NotALeafNodeException { throws NoResponseException, NotConnectedException, XMPPErrorException, InterruptedException, PubSubException.NotALeafNodeException {
DataExtension dataExtension = new DataExtension(data); DataExtension dataExtension = new DataExtension(data);
getOrCreateDataNode().publish(new PayloadItem<>(itemId, dataExtension)); getOrCreateDataNode().publish(new PayloadItem<>(itemId, dataExtension));
} }
/** /**
* Publish metadata about an avatar to the metadata node. * Publish metadata about an avatar of type {@link #TYPE_PNG} to the metadata node.
* *
* @param itemId SHA-1 sum of the image of type image/png * @param itemId SHA-1 sum of the image of type {@link #TYPE_PNG}
* @param info info element containing metadata of the file * @param info info element containing metadata of the file
* @param pointers list of metadata pointer elements * @param pointers optional list of metadata pointer elements
* *
* @throws NoResponseException * @throws NoResponseException if the server does not respond
* @throws XMPPErrorException * @throws XMPPErrorException if a protocol level error occurs
* @throws NotConnectedException * @throws NotConnectedException of the connection is not connected
* @throws InterruptedException * @throws InterruptedException if the thread is interrupted
* @throws PubSubException.NotALeafNodeException if the metadata node is not a {@link LeafNode}
* @throws UserAvatarException.AvatarMetadataMissingPNGInfoException if the info element does not point to an
* avatar image of type {@link #TYPE_PNG} available in PubSub.
*/ */
public void publishAvatarMetadata(String itemId, MetadataInfo info, List<MetadataPointer> pointers) public void publishPNGAvatarMetadata(String itemId, MetadataInfo info, List<MetadataPointer> pointers)
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, PubSubException.NotALeafNodeException { throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, PubSubException.NotALeafNodeException {
publishAvatarMetadata(itemId, Collections.singletonList(info), pointers); publishAvatarMetadata(itemId, Collections.singletonList(info), pointers);
} }
/** /**
* Publish avatar metadata. * Publish avatar metadata.
* The list of {@link MetadataInfo} elements can contain info about several image data types. However, there must
* be at least one {@link MetadataInfo} element about an image of type {@link #TYPE_PNG} which is destined to
* publication in PubSub. Its id MUST equal the itemId parameter.
* *
* @param itemId SHA-1 sum of the avatar image representation of type image/png * @param itemId SHA-1 sum of the avatar image representation of type {@link #TYPE_PNG}
* @param infos list of metadata elements * @param infos list of metadata elements
* @param pointers list of pointer elements * @param pointers optional list of pointer elements
* *
* @throws NoResponseException * @throws NoResponseException if the server does not respond
* @throws XMPPErrorException * @throws XMPPErrorException if a protocol level error occurs
* @throws NotConnectedException * @throws NotConnectedException if the connection is not connected
* @throws InterruptedException * @throws InterruptedException if the thread is interrupted
* @throws PubSubException.NotALeafNodeException if the metadata node is not a {@link LeafNode}
* @throws UserAvatarException.AvatarMetadataMissingPNGInfoException if the list of {@link MetadataInfo} elements
* does not contain at least one PNG image
*
* @see <a href="https://xmpp.org/extensions/xep-0084.html#proto-info">
* §4.2.1 Info Element - About the restriction that at least one info element must describe a PNG image.</a>
*/ */
public void publishAvatarMetadata(String itemId, List<MetadataInfo> infos, List<MetadataPointer> pointers) public void publishAvatarMetadata(String itemId, List<MetadataInfo> infos, List<MetadataPointer> pointers)
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, PubSubException.NotALeafNodeException { throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, PubSubException.NotALeafNodeException {
// Check if metadata extension contains at least one png image available in PubSub
boolean containsPng = false;
for (MetadataInfo info : infos) {
if (TYPE_PNG.equals(info.getType())) {
containsPng = true;
break;
}
}
if (!containsPng) {
throw new UserAvatarException.AvatarMetadataMissingPNGInfoException(
"The MetadataExtension must contain at least one info element describing an image of type " +
"\"" + TYPE_PNG + "\"");
}
MetadataExtension metadataExtension = new MetadataExtension(infos, pointers); MetadataExtension metadataExtension = new MetadataExtension(infos, pointers);
getOrCreateMetadataNode().publish(new PayloadItem<>(itemId, metadataExtension)); getOrCreateMetadataNode().publish(new PayloadItem<>(itemId, metadataExtension));
@ -309,46 +367,46 @@ public final class UserAvatarManager extends Manager {
} }
/** /**
* Publish metadata about an avatar available via HTTP. * Publish metadata about a PNG avatar available via HTTP.
* This method can be used together with HTTP File Upload as an alternative to PubSub for avatar publishing. * 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 itemId SHA-1 sum of the avatar image file.
* @param url HTTP(S) Url of the image file. * @param url HTTP(S) Url of the image file.
* @param bytes size of the file in bytes * @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 pixelsHeight height of the image file in pixels
* @param pixelsWidth width of the image file in pixels * @param pixelsWidth width of the image file in pixels
* *
* @throws NoResponseException * @throws NoResponseException if the server does not respond
* @throws XMPPErrorException * @throws XMPPErrorException of a protocol level error occurs
* @throws NotConnectedException * @throws NotConnectedException if the connection is not connected
* @throws InterruptedException * @throws InterruptedException if the thread is interrupted
* @throws PubSubException.NotALeafNodeException if the metadata node is not a {@link LeafNode}
*/ */
public void publishHttpAvatarMetadata(String itemId, URL url, long bytes, String type, public void publishHttpPNGAvatarMetadata(String itemId, URL url, long bytes,
int pixelsHeight, int pixelsWidth) int pixelsHeight, int pixelsWidth)
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, PubSubException.NotALeafNodeException { throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, PubSubException.NotALeafNodeException {
MetadataInfo info = new MetadataInfo(itemId, url, bytes, type, pixelsHeight, pixelsWidth); MetadataInfo info = new MetadataInfo(itemId, url, bytes, TYPE_PNG, pixelsHeight, pixelsWidth);
publishAvatarMetadata(itemId, info, null); publishPNGAvatarMetadata(itemId, info, null);
} }
/** /**
* Publish avatar metadata with its size in pixels. * Publish avatar metadata about a PNG avatar with its size in pixels.
* *
* @param itemId * @param itemId SHA-1 hash of the PNG encoded image
* @param bytes * @param bytes number of bytes of this particular image data array
* @param type * @param pixelsHeight height of this image in pixels
* @param pixelsHeight * @param pixelsWidth width of this image in pixels
* @param pixelsWidth *
* @throws NoResponseException * @throws NoResponseException if the server does not respond
* @throws XMPPErrorException * @throws XMPPErrorException if a protocol level error occurs
* @throws NotConnectedException * @throws NotConnectedException if the connection is not connected
* @throws InterruptedException * @throws PubSubException.NotALeafNodeException if the metadata node is not a {@link LeafNode}
* @throws InterruptedException if the thread is interrupted
*/ */
public void publishAvatarMetadata(String itemId, long bytes, String type, int pixelsHeight, public void publishPNGAvatarMetadata(String itemId, long bytes, int pixelsHeight, int pixelsWidth)
int pixelsWidth)
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, PubSubException.NotALeafNodeException { throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, PubSubException.NotALeafNodeException {
MetadataInfo info = new MetadataInfo(itemId, null, bytes, type, pixelsHeight, pixelsWidth); MetadataInfo info = new MetadataInfo(itemId, null, bytes, TYPE_PNG, pixelsHeight, pixelsWidth);
publishAvatarMetadata(itemId, info, null); publishPNGAvatarMetadata(itemId, info, null);
} }
/** /**
@ -356,10 +414,11 @@ public final class UserAvatarManager extends Manager {
* *
* @see <a href="https://xmpp.org/extensions/xep-0084.html#proto-meta">§4.2 Metadata Element</a> * @see <a href="https://xmpp.org/extensions/xep-0084.html#proto-meta">§4.2 Metadata Element</a>
* *
* @throws NoResponseException * @throws NoResponseException if the server does not respond
* @throws XMPPErrorException * @throws XMPPErrorException if a protocol level error occurs
* @throws NotConnectedException * @throws NotConnectedException if the connection is not connected
* @throws InterruptedException * @throws InterruptedException if the thread is interrupted
* @throws PubSubException.NotALeafNodeException if the metadata node is not a {@link LeafNode}
*/ */
public void unpublishAvatar() public void unpublishAvatar()
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, PubSubException.NotALeafNodeException { throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, PubSubException.NotALeafNodeException {

View File

@ -21,7 +21,6 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smack.packet.XmlEnvironment; import org.jivesoftware.smack.packet.XmlEnvironment;
import org.jivesoftware.smack.util.XmlStringBuilder; import org.jivesoftware.smack.util.XmlStringBuilder;
import org.jivesoftware.smackx.avatar.MetadataInfo; import org.jivesoftware.smackx.avatar.MetadataInfo;
@ -47,7 +46,7 @@ public class MetadataExtension implements ExtensionElement {
/** /**
* Metadata Extension constructor. * Metadata Extension constructor.
* *
* @param infos * @param infos list of {@link MetadataInfo} elements.
*/ */
public MetadataExtension(List<MetadataInfo> infos) { public MetadataExtension(List<MetadataInfo> infos) {
this(infos, null); this(infos, null);
@ -56,8 +55,8 @@ public class MetadataExtension implements ExtensionElement {
/** /**
* Metadata Extension constructor. * Metadata Extension constructor.
* *
* @param infos * @param infos list of {@link MetadataInfo} elements
* @param pointers * @param pointers optional list of {@link MetadataPointer} elements
*/ */
public MetadataExtension(List<MetadataInfo> infos, List<MetadataPointer> pointers) { public MetadataExtension(List<MetadataInfo> infos, List<MetadataPointer> pointers) {
this.infos = infos; this.infos = infos;
@ -82,6 +81,15 @@ public class MetadataExtension implements ExtensionElement {
return (pointers == null) ? null : Collections.unmodifiableList(pointers); return (pointers == null) ? null : Collections.unmodifiableList(pointers);
} }
/**
* 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();
}
@Override @Override
public String getElementName() { public String getElementName() {
return ELEMENT; return ELEMENT;
@ -160,14 +168,4 @@ public class MetadataExtension implements ExtensionElement {
xml.closeEmptyElement(); 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();
}
} }