From fd78deedb1430bab20c17baa63b33df4bcd8e9be Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 16 Aug 2020 15:14:36 +0200 Subject: [PATCH] Prototype PubSubUri class --- .../jivesoftware/smackx/pubsub/PubSubUri.java | 215 ++++++++++++++++++ .../smackx/pubsub/PubSubUriTest.java | 155 +++++++++++++ 2 files changed, 370 insertions(+) create mode 100644 domain/src/main/java/org/jivesoftware/smackx/pubsub/PubSubUri.java create mode 100644 domain/src/test/java/org/jivesoftware/smackx/pubsub/PubSubUriTest.java diff --git a/domain/src/main/java/org/jivesoftware/smackx/pubsub/PubSubUri.java b/domain/src/main/java/org/jivesoftware/smackx/pubsub/PubSubUri.java new file mode 100644 index 0000000..95ecbed --- /dev/null +++ b/domain/src/main/java/org/jivesoftware/smackx/pubsub/PubSubUri.java @@ -0,0 +1,215 @@ +package org.jivesoftware.smackx.pubsub; + +import org.jxmpp.jid.BareJid; +import org.jxmpp.jid.EntityBareJid; +import org.jxmpp.jid.impl.JidCreate; +import org.jxmpp.stringprep.XmppStringprepException; +import org.jxmpp.util.XmppStringUtils; + +import java.net.URI; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class PubSubUri { + + private static final Logger LOGGER = Logger.getLogger(PubSubUri.class.getName()); + + public static final String SCHEME = "xmpp"; + public static final String NODE = ";node="; + public static final String ITEM = ";item="; + public static final String ACTION = ";action="; + public static final String PUBSUB = "pubsub"; + + private final BareJid service; + private final String node; + private final String item; + private final Action action; + + public PubSubUri(BareJid service, String node, String item, Action action) { + this.service = service; + this.node = node; + this.item = item; + this.action = action; + } + + /** + * Parse a {@link PubSubUri}. + * This method ignores unknown parameters. + * + * @param s string representation of the URI. + * @return parsed {@link PubSubUri} + * @throws XmppStringprepException if the string does not represent a valid XMPP PubSub URI. + */ + public static PubSubUri from(String s) + throws XmppStringprepException { + URI uri = URI.create(s); + throwIfNoXmppUri(uri); + String ssp = uri.getSchemeSpecificPart(); + BareJid service = getService(ssp); + String node = getNodeOrNull(ssp); + String item = getItemOrNull(ssp); + Action action = getActionOrNull(ssp); + + return new PubSubUri(service, node, item, action); + } + + /** + * Return the {@link BareJid} of the referenced pubsub service. + * That may be a {@link org.jxmpp.jid.DomainBareJid} like 'pubsub.shakespeare.lit' or a + * {@link EntityBareJid} like 'hamlet@denmark.lit'. + * + * @return pubsub service jid + */ + public BareJid getService() { + return service; + } + + /** + * Return the pubsub node name or null if not present. + * + * @return node name or null + */ + public String getNode() { + return node; + } + + /** + * Return the pubsub item name or null if not present. + * + * @return item or null + */ + public String getItem() { + return item; + } + + /** + * Return the {@link Action} of the {@link PubSubUri} or null if not present. + * + * @return action or null + */ + public Action getAction() { + return action; + } + + /** + * Throw an {@link IllegalArgumentException} if the {@link URI URIs} scheme doesn't equal 'xmpp'. + * + * @param uri uri + */ + private static void throwIfNoXmppUri(URI uri) throws XmppStringprepException { + if (!uri.getScheme().equals(SCHEME)) { + throw new XmppStringprepException(uri.toString(), "URI is not of scheme 'xmpp'."); + } + } + + /** + * Extract the {@link BareJid} of the pubsub service the URI is pointing to. + * + * @param ssp scheme specific part of the URI + * @return pubsub service jid + * + * @throws XmppStringprepException if the service jid is malformed. + */ + private static BareJid getService(String ssp) throws XmppStringprepException { + String serviceString = ssp.substring(0, ssp.indexOf("?")); + // TODO: Use JidCreate.bareFrom(serviceString) once jxmpp is bumped to > 1.0.0 + // see https://github.com/igniterealtime/jxmpp/pull/22 + throwIfFullJid(serviceString); + BareJid jid; + try { + jid = JidCreate.entityBareFromUrlEncoded(serviceString); + } catch (XmppStringprepException e) { + jid = JidCreate.domainBareFromUrlEncoded(serviceString); + } + return jid; + } + + /** + * Throw an {@link XmppStringprepException} if the provided {@link String} represents a {@link org.jxmpp.jid.FullJid}. + * + * @param jid jid as string + * @throws XmppStringprepException if jid has resource part + */ + private static void throwIfFullJid(String jid) throws XmppStringprepException { + if (XmppStringUtils.isFullJID(jid)) { + throw new XmppStringprepException(jid, "FullJid not allowed here."); + } + } + + /** + * Return the value of the 'node' key or null if not present. + * @param ssp scheme specific part + * @return node value or null + */ + private static String getNodeOrNull(String ssp) { + return getValueOrNull(ssp, NODE); + } + + + /** + * Return the value of the 'item' key or null if not present. + * @param ssp scheme specific part + * @return item value or null + */ + private static String getItemOrNull(String ssp) { + return getValueOrNull(ssp, ITEM); + } + + + /** + * Return the value of the 'action' key or null if not present. + * @param ssp scheme specific part + * @return action value or null + */ + private static Action getActionOrNull(String ssp) { + String actionString = getValueOrNull(ssp, ACTION); + if (actionString == null) { + return null; + } + if (!ssp.substring(ssp.indexOf("?") + 1).startsWith(PUBSUB)) { + LOGGER.log(Level.WARNING, "URI contains 'action', but does not have iquerytype set to '" + PUBSUB + "'.\n" + + "See https://tools.ietf.org/html/rfc5122#section-2.2 and https://xmpp.org/extensions/xep-0060.html#example-229 f."); + } + return Action.valueOf(actionString); + } + + /** + * Returns the ivalue value of the ipair with ikey key. + * + * @see ABNF in section 2.2 of rfc5122 + * @param ssp scheme specific part of the XMPP URI/IRI. + * @param key ikey + * @return ivalue + */ + private static String getValueOrNull(String ssp, String key) { + String args = ssp.substring(ssp.indexOf("?") + 1); + boolean hasKey = args.contains(key); + if (!hasKey) return null; + + int keyPos = args.indexOf(key) + key.length(); + int nextPos = args.indexOf(";", keyPos); + if (nextPos == -1) { + nextPos = args.length(); + } + return args.substring(keyPos, nextPos); + } + + @Override + public String toString() { + return SCHEME + ":" + getService() + "?" + + (getAction() != null ? PUBSUB + ACTION + getAction() : "") + + (getNode() != null ? NODE + getNode() : "") + + (getItem() != null ? ITEM + getItem() : ""); + } + + /** + * PubSub specific actions as defined in XEP-0060: Publish-Subscribe: ยง16.6 URI Query Types. + * + * @see Registered URI Query Types + */ + public enum Action { + subscribe, + unsubscribe, + retrieve + } +} diff --git a/domain/src/test/java/org/jivesoftware/smackx/pubsub/PubSubUriTest.java b/domain/src/test/java/org/jivesoftware/smackx/pubsub/PubSubUriTest.java new file mode 100644 index 0000000..50ae54c --- /dev/null +++ b/domain/src/test/java/org/jivesoftware/smackx/pubsub/PubSubUriTest.java @@ -0,0 +1,155 @@ +package org.jivesoftware.smackx.pubsub; + +import org.junit.Test; +import org.jxmpp.jid.DomainBareJid; +import org.jxmpp.jid.EntityBareJid; +import org.jxmpp.jid.impl.JidCreate; +import org.jxmpp.stringprep.XmppStringprepException; + +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertNull; +import static junit.framework.TestCase.assertTrue; +import static org.junit.Assert.assertThrows; + +public class PubSubUriTest { + + /** + * Parse a URI that points to a pubsub node. + * + * @see + * XEP-0060: Publish-Subscribe: Example 227. XMPP URI for a node + * @throws XmppStringprepException not expected + */ + @Test + public void pubSubNodeUriTest() throws XmppStringprepException { + final String uriString = "xmpp:pubsub.shakespeare.lit?;node=princely_musings"; + PubSubUri uri = PubSubUri.from(uriString); + + assertEquals(JidCreate.domainBareFromOrThrowUnchecked("pubsub.shakespeare.lit"), uri.getService()); + assertEquals("princely_musings", uri.getNode()); + assertNull(uri.getItem()); + assertNull(uri.getAction()); + assertEquals(uriString, uri.toString()); + } + + /** + * Parse a URI that points to an item inside a pubsub node. + * + * @see + * XEP-0060: Publish-Subscribe: Example 228. XMPP URI for a pubsub item + * @throws XmppStringprepException not expected + */ + @Test + public void pubSubItemInNodeUriTest() throws XmppStringprepException { + final String uriString = "xmpp:pubsub.shakespeare.lit?;node=princely_musings;item=ae890ac52d0df67ed7cfdf51b644e901"; + PubSubUri uri = PubSubUri.from(uriString); + + assertEquals(JidCreate.domainBareFromOrThrowUnchecked("pubsub.shakespeare.lit"), uri.getService()); + assertEquals("princely_musings", uri.getNode()); + assertEquals("ae890ac52d0df67ed7cfdf51b644e901", uri.getItem()); + assertNull(uri.getAction()); + assertEquals(uriString, uri.toString()); + } + + /** + * Parse a URI for subscribing to a pubsub node. + * + * @see + * XEP-0060: Publish-Subscribe: Example 229. URI for subscribing to a pubsub node + * @throws XmppStringprepException not expected + */ + @Test + public void pubSubSubscribeToNodeUriTest() throws XmppStringprepException { + final String uriString = "xmpp:pubsub.shakespeare.lit?pubsub;action=subscribe;node=princely_musings"; + PubSubUri uri = PubSubUri.from(uriString); + + assertEquals(JidCreate.domainBareFromOrThrowUnchecked("pubsub.shakespeare.lit"), uri.getService()); + assertEquals("princely_musings", uri.getNode()); + assertNull(uri.getItem()); + assertEquals(PubSubUri.Action.subscribe, uri.getAction()); + assertEquals(uriString, uri.toString()); + } + + /** + * Parse a URI for retrieving a single item from a specific node. + * + * @see + * XEP-0060: Publish-Subscribe: Example 230. URI for retrieving a pubsub item + * @throws XmppStringprepException not expected + */ + @Test + public void pubSubRetrieveItemFromNodeUriTest() throws XmppStringprepException { + final String uriString = "xmpp:pubsub.shakespeare.lit?pubsub;action=retrieve;node=princely_musings;item=ae890ac52d0df67ed7cfdf51b644e901"; + PubSubUri uri = PubSubUri.from(uriString); + + assertEquals(JidCreate.domainBareFromOrThrowUnchecked("pubsub.shakespeare.lit"), uri.getService()); + assertEquals(PubSubUri.Action.retrieve, uri.getAction()); + assertEquals("princely_musings", uri.getNode()); + assertEquals("ae890ac52d0df67ed7cfdf51b644e901", uri.getItem()); + assertEquals(uriString, uri.toString()); + } + + /** + * Parse a URI for unsubscribing from a specific pubsub node. + * + * @see + * XEP-0060: Publish-Subscribe: Example 233. Pubsub Unsubscribe Action: IRI/URI + * @throws XmppStringprepException not expected + */ + @Test + public void pubSubUnsubscribeFromNodeUriTest() throws XmppStringprepException { + final String uriString = "xmpp:pubsub.shakespeare.lit?pubsub;action=unsubscribe;node=princely_musings"; + PubSubUri uri = PubSubUri.from(uriString); + + assertEquals(JidCreate.domainBareFromOrThrowUnchecked("pubsub.shakespeare.lit"), uri.getService()); + assertEquals(PubSubUri.Action.unsubscribe, uri.getAction()); + assertEquals("princely_musings", uri.getNode()); + assertNull(uri.getItem()); + assertEquals(uriString, uri.toString()); + } + + /** + * Parse a URI for retrieving multiple items from a particular pubsub node. + * @see + * XEP-0060: Publish-Subscribe: Example 235. Pubsub Retrieve Action: IRI/URI + */ + @Test + public void pubSubRetrieveItemsFromNodeUriTest() throws XmppStringprepException { + final String uriString = "xmpp:pubsub.shakespeare.lit?pubsub;action=retrieve;node=princely_musings"; + PubSubUri uri = PubSubUri.from(uriString); + + assertEquals(JidCreate.domainBareFromOrThrowUnchecked("pubsub.shakespeare.lit"), uri.getService()); + assertEquals(PubSubUri.Action.retrieve, uri.getAction()); + assertEquals("princely_musings", uri.getNode()); + assertNull(uri.getItem()); + assertEquals(uriString, uri.toString()); + } + + @Test + public void serviceJidsAreParsedToCorrectJidTypes() throws XmppStringprepException { + final String uriWithDomainJid = "xmpp:pubsub.shakespeare.lit?;node=princely_musings"; + final String uriWithEntityJid = "xmpp:hamlet@denmark.lit?;node=blog"; + + PubSubUri withDomainJid = PubSubUri.from(uriWithDomainJid); + PubSubUri withEntityJid = PubSubUri.from(uriWithEntityJid); + + assertTrue(withDomainJid.getService() instanceof DomainBareJid); + assertTrue(withEntityJid.getService() instanceof EntityBareJid); + } + + /** + * Ensure that parsing a uri with an invalid scheme ('xmpps' in this case) will cause a + * {@link XmppStringprepException} to be thrown. + */ + @Test + public void invalidSchemeCausesThrowingTest() { + assertThrows(XmppStringprepException.class, + () -> PubSubUri.from("xmpps:pubsub.shakespeare.lit?;node=princely_musings")); + } + + @Test + public void fullJidCausesThrowingTest() { + assertThrows(XmppStringprepException.class, + () -> PubSubUri.from("xmpp:alice@wonderland.lit/illegalResourcePart?:node=rabbit_hole")); + } +}