From bfefbdb7774283bcb0962dd62877d8601bfdcfb6 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 4 Feb 2013 09:53:56 +0000 Subject: [PATCH] Implement XEP-0184 delivery notifications This patch provides three components required to implement XEP-0184: * DeliveryReceiptRequest is a PacketExtension to request a receipt * DeliveryReceipt is a PacketExtension that contains the receipt * DeliveryReceiptManager to handle sending/receiving of requests and receipts. Implementation: For requesting a receipt, just add a new DeliveryReceiptRequest() to your message or use the helper function: DeliveryReceiptManager.addDeliveryReceiptRequest(packet); Register a ReceiptReceivedListener to find out if your packet arrived: DeliveryReceiptManager.getInstanceFor(myConnection) .registerReceiptReceivedListener(new ReceiptReceivedListener() { @Override public void onReceiptReceived(String fromJid, String toJid, String receiptId) { // receipt received for packet id receiptId } To answer a receipt request, you can either check incoming packets manually: if (DeliveryReceiptManager.hasDeliveryReceiptRequest(packet)) { // send receipt } Or you can enable automatic receipt transmission with: DeliveryReceiptManager.getInstanceFor(myConnection) .setAutoReceiptsEnabled(true); Signed-Off-By: Georg Lukas git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/trunk@13431 b35dd754-fafc-0310-a699-88a17e54d16e --- build/resources/META-INF/smack.providers | 12 + .../smackx/receipts/DeliveryReceipt.java | 74 ++++++ .../receipts/DeliveryReceiptManager.java | 216 ++++++++++++++++++ .../receipts/DeliveryReceiptRequest.java | 54 +++++ .../smackx/receipts/DeliveryReceiptTest.java | 141 ++++++++++++ 5 files changed, 497 insertions(+) create mode 100644 source/org/jivesoftware/smackx/receipts/DeliveryReceipt.java create mode 100644 source/org/jivesoftware/smackx/receipts/DeliveryReceiptManager.java create mode 100644 source/org/jivesoftware/smackx/receipts/DeliveryReceiptRequest.java create mode 100644 test-unit/org/jivesoftware/smackx/receipts/DeliveryReceiptTest.java diff --git a/build/resources/META-INF/smack.providers b/build/resources/META-INF/smack.providers index 2aaf8a3ba..0de71aa04 100644 --- a/build/resources/META-INF/smack.providers +++ b/build/resources/META-INF/smack.providers @@ -666,4 +666,16 @@ urn:xmpp:ping org.jivesoftware.smackx.ping.provider.PingProvider + + + + received + urn:xmpp:receipts + org.jivesoftware.smackx.receipts.DeliveryReceipt$Provider + + + request + urn:xmpp:receipts + org.jivesoftware.smackx.receipts.DeliveryReceiptRequest$Provider + diff --git a/source/org/jivesoftware/smackx/receipts/DeliveryReceipt.java b/source/org/jivesoftware/smackx/receipts/DeliveryReceipt.java new file mode 100644 index 000000000..14f4456d2 --- /dev/null +++ b/source/org/jivesoftware/smackx/receipts/DeliveryReceipt.java @@ -0,0 +1,74 @@ +/* + * All rights reserved. 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.receipts; + +import java.util.List; +import java.util.Map; + +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.provider.EmbeddedExtensionProvider; + +/** + * Represents a message delivery receipt entry as specified by + * Message Delivery Receipts. + * + * @author Georg Lukas + */ +public class DeliveryReceipt implements PacketExtension +{ + public static final String NAMESPACE = "urn:xmpp:receipts"; + public static final String ELEMENT = "received"; + + private String id; /// original ID of the delivered message + + public DeliveryReceipt(String id) + { + this.id = id; + } + + public String getId() + { + return id; + } + + public String getElementName() + { + return ELEMENT; + } + + public String getNamespace() + { + return NAMESPACE; + } + + public String toXML() + { + return ""; + } + + /** + * This Provider parses and returns DeliveryReceipt packets. + */ + public static class Provider extends EmbeddedExtensionProvider + { + + @Override + protected PacketExtension createReturnExtension(String currentElement, String currentNamespace, + Map attributeMap, List content) + { + return new DeliveryReceipt(attributeMap.get("id")); + } + + } +} diff --git a/source/org/jivesoftware/smackx/receipts/DeliveryReceiptManager.java b/source/org/jivesoftware/smackx/receipts/DeliveryReceiptManager.java new file mode 100644 index 000000000..eaac5a4c4 --- /dev/null +++ b/source/org/jivesoftware/smackx/receipts/DeliveryReceiptManager.java @@ -0,0 +1,216 @@ +/** + * Copyright 2013 Georg Lukas + * + * All rights reserved. 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.receipts; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; + +import org.jivesoftware.smack.Connection; +import org.jivesoftware.smack.ConnectionCreationListener; +import org.jivesoftware.smack.PacketCollector; +import org.jivesoftware.smack.PacketListener; +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.PacketExtensionFilter; +import org.jivesoftware.smack.filter.PacketIDFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smackx.ServiceDiscoveryManager; +import org.jivesoftware.smackx.packet.DiscoverInfo; + +/** + * Packet extension for XEP-0184: Message Delivery Receipts. This class implements + * the manager for {@link DeliveryReceipt} support, enabling and disabling of + * automatic DeliveryReceipt transmission. + * + * @author Georg Lukas + */ +public class DeliveryReceiptManager implements PacketListener { + + private static Map instances = + Collections.synchronizedMap(new WeakHashMap()); + + static { + Connection.addConnectionCreationListener(new ConnectionCreationListener() { + public void connectionCreated(Connection connection) { + new DeliveryReceiptManager(connection); + } + }); + } + + private Connection connection; + private boolean auto_receipts_enabled = false; + private Set receiptReceivedListeners = Collections + .synchronizedSet(new HashSet()); + + private DeliveryReceiptManager(Connection connection) { + ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection); + sdm.addFeature(DeliveryReceipt.NAMESPACE); + this.connection = connection; + instances.put(connection, this); + + // register listener for delivery receipts and requests + connection.addPacketListener(this, new PacketExtensionFilter(DeliveryReceipt.NAMESPACE)); + } + + /** + * Obtain the DeliveryReceiptManager responsible for a connection. + * + * @param connection the connection object. + * + * @return the DeliveryReceiptManager instance for the given connection + */ + synchronized public static DeliveryReceiptManager getInstanceFor(Connection connection) { + DeliveryReceiptManager receiptManager = instances.get(connection); + + if (receiptManager == null) { + receiptManager = new DeliveryReceiptManager(connection); + } + + return receiptManager; + } + + /** + * Returns true if Delivery Receipts are supported by a given JID + * + * @param jid + * @return true if supported + */ + public boolean isSupported(String jid) { + try { + DiscoverInfo result = + ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(jid); + return result.containsFeature(DeliveryReceipt.NAMESPACE); + } + catch (XMPPException e) { + return false; + } + } + + // handle incoming receipts and receipt requests + @Override + public void processPacket(Packet packet) { + DeliveryReceipt dr = (DeliveryReceipt)packet.getExtension( + DeliveryReceipt.ELEMENT, DeliveryReceipt.NAMESPACE); + if (dr != null) { + // notify listeners of incoming receipt + for (ReceiptReceivedListener l : receiptReceivedListeners) { + l.onReceiptReceived(packet.getFrom(), packet.getTo(), dr.getId()); + } + + } + + // if enabled, automatically send a receipt + if (auto_receipts_enabled) { + DeliveryReceiptRequest drr = (DeliveryReceiptRequest)packet.getExtension( + DeliveryReceiptRequest.ELEMENT, DeliveryReceipt.NAMESPACE); + if (drr != null) { + Message ack = new Message(packet.getFrom(), Message.Type.normal); + ack.addExtension(new DeliveryReceipt(packet.getPacketID())); + connection.sendPacket(ack); + } + } + } + + /** + * Configure whether the {@link DeliveryReceiptManager} should automatically + * reply to incoming {@link DeliveryReceipt}s. By default, this feature is off. + * + * @param new_state whether automatic transmission of + * DeliveryReceipts should be enabled or disabled + */ + public void setAutoReceiptsEnabled(boolean new_state) { + auto_receipts_enabled = new_state; + } + + /** + * Helper method to enable automatic DeliveryReceipt transmission. + */ + public void enableAutoReceipts() { + setAutoReceiptsEnabled(true); + } + + /** + * Helper method to disable automatic DeliveryReceipt transmission. + */ + public void disableAutoReceipts() { + setAutoReceiptsEnabled(false); + } + + /** + * Check if AutoReceipts are enabled on this connection. + */ + public boolean getAutoReceiptsEnabled() { + return this.auto_receipts_enabled; + } + + /** + * Get informed about incoming delivery receipts with a {@link ReceiptReceivedListener}. + * + * @param listener the listener to be informed about new receipts + */ + public void registerReceiptReceivedListener(ReceiptReceivedListener listener) { + receiptReceivedListeners.add(listener); + } + + /** + * Stop getting informed about incoming delivery receipts. + * + * @param listener the listener to be removed + */ + public void unregisterReceiptReceivedListener(ReceiptReceivedListener listener) { + receiptReceivedListeners.remove(listener); + } + + /** + * Interface for received receipt notifications. + * + * Implement this and add a listener to get notified. + */ + public static interface ReceiptReceivedListener { + void onReceiptReceived(String fromJid, String toJid, String receiptId); + } + + + /** + * Test if a packet requires a delivery receipt. + * + * @param p Packet object to check for a DeliveryReceiptRequest + * + * @return true if a delivery receipt was requested + */ + public static boolean hasDeliveryReceiptRequest(Packet p) { + return (p.getExtension(DeliveryReceiptRequest.ELEMENT, + DeliveryReceipt.NAMESPACE) != null); + } + + /** + * Add a delivery receipt request to an outgoing packet. + * + * Only message packets may contain receipt requests as of XEP-0184, + * therefore only allow Message as the parameter type. + * + * @param m Message object to add a request to + */ + public static void addDeliveryReceiptRequest(Message m) { + m.addExtension(new DeliveryReceiptRequest()); + } +} diff --git a/source/org/jivesoftware/smackx/receipts/DeliveryReceiptRequest.java b/source/org/jivesoftware/smackx/receipts/DeliveryReceiptRequest.java new file mode 100644 index 000000000..1b5ed3bc4 --- /dev/null +++ b/source/org/jivesoftware/smackx/receipts/DeliveryReceiptRequest.java @@ -0,0 +1,54 @@ +/* + * All rights reserved. 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.receipts; + +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.provider.PacketExtensionProvider; +import org.xmlpull.v1.XmlPullParser; + +/** + * Represents a message delivery receipt request entry as specified by + * Message Delivery Receipts. + * + * @author Georg Lukas + */ +public class DeliveryReceiptRequest implements PacketExtension +{ + public static final String ELEMENT = "request"; + + public String getElementName() + { + return ELEMENT; + } + + public String getNamespace() + { + return DeliveryReceipt.NAMESPACE; + } + + public String toXML() + { + return ""; + } + + /** + * This Provider parses and returns DeliveryReceiptRequest packets. + */ + public static class Provider implements PacketExtensionProvider { + @Override + public PacketExtension parseExtension(XmlPullParser parser) { + return new DeliveryReceiptRequest(); + } + } +} diff --git a/test-unit/org/jivesoftware/smackx/receipts/DeliveryReceiptTest.java b/test-unit/org/jivesoftware/smackx/receipts/DeliveryReceiptTest.java new file mode 100644 index 000000000..d6e51d740 --- /dev/null +++ b/test-unit/org/jivesoftware/smackx/receipts/DeliveryReceiptTest.java @@ -0,0 +1,141 @@ +/** + * All rights reserved. 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.receipts; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.io.StringReader; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Properties; +import java.util.TimeZone; + +import org.jivesoftware.smack.DummyConnection; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.util.PacketParserUtils; +import org.jivesoftware.smackx.ServiceDiscoveryManager; +import org.junit.Test; +import org.xmlpull.mxp1.MXParser; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import com.jamesmurty.utils.XMLBuilder; + +public class DeliveryReceiptTest { + + private static Properties outputProperties = new Properties(); + static { + outputProperties.put(javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION, "yes"); + } + + @Test + public void receiptTest() throws Exception { + XmlPullParser parser; + String control; + + control = XMLBuilder.create("message") + .a("from", "romeo@montague.com") + .e("request") + .a("xmlns", "urn:xmpp:receipts") + .asString(outputProperties); + + parser = getParser(control, "message"); + Packet p = PacketParserUtils.parseMessage(parser); + + DeliveryReceiptRequest drr = (DeliveryReceiptRequest)p.getExtension( + DeliveryReceiptRequest.ELEMENT, DeliveryReceipt.NAMESPACE); + assertNotNull(drr); + + assertTrue(DeliveryReceiptManager.hasDeliveryReceiptRequest(p)); + + Message m = new Message("romeo@montague.com", Message.Type.normal); + assertFalse(DeliveryReceiptManager.hasDeliveryReceiptRequest(m)); + DeliveryReceiptManager.addDeliveryReceiptRequest(m); + assertTrue(DeliveryReceiptManager.hasDeliveryReceiptRequest(m)); + } + + @Test + public void receiptManagerListenerTest() throws Exception { + DummyConnection c = new DummyConnection(); + ServiceDiscoveryManager sdm = new ServiceDiscoveryManager(c); + DeliveryReceiptManager drm = DeliveryReceiptManager.getInstanceFor(c); + + TestReceiptReceivedListener rrl = new TestReceiptReceivedListener(); + drm.registerReceiptReceivedListener(rrl); + + Message m = new Message("romeo@montague.com", Message.Type.normal); + m.setFrom("julia@capulet.com"); + m.setPacketID("reply-id"); + m.addExtension(new DeliveryReceipt("original-test-id")); + drm.processPacket(m); + + // ensure the listener got called + assertEquals("original-test-id", rrl.receiptId); + } + + private static class TestReceiptReceivedListener implements DeliveryReceiptManager.ReceiptReceivedListener { + public String receiptId = null; + @Override + public void onReceiptReceived(String fromJid, String toJid, String receiptId) { + assertEquals("julia@capulet.com", fromJid); + assertEquals("romeo@montague.com", toJid); + assertEquals("original-test-id", receiptId); + this.receiptId = receiptId; + } + } + + @Test + public void receiptManagerAutoReplyTest() throws Exception { + DummyConnection c = new DummyConnection(); + ServiceDiscoveryManager sdm = new ServiceDiscoveryManager(c); + DeliveryReceiptManager drm = DeliveryReceiptManager.getInstanceFor(c); + + drm.enableAutoReceipts(); + assertTrue(drm.getAutoReceiptsEnabled()); + + // test auto-receipts + Message m = new Message("julia@capulet.com", Message.Type.normal); + m.setFrom("romeo@montague.com"); + m.setPacketID("test-receipt-request"); + DeliveryReceiptManager.addDeliveryReceiptRequest(m); + + // the DRM will send a reply-packet + assertEquals(0, c.getNumberOfSentPackets()); + drm.processPacket(m); + assertEquals(1, c.getNumberOfSentPackets()); + + Packet reply = c.getSentPacket(); + DeliveryReceipt r = (DeliveryReceipt)reply.getExtension("received", "urn:xmpp:receipts"); + assertEquals("romeo@montague.com", reply.getTo()); + assertEquals("test-receipt-request", r.getId()); + } + + private XmlPullParser getParser(String control, String startTag) + throws XmlPullParserException, IOException { + XmlPullParser parser = new MXParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + parser.setInput(new StringReader(control)); + while (true) { + if (parser.next() == XmlPullParser.START_TAG + && parser.getName().equals(startTag)) { + break; + } + } + return parser; + } +}