Prototype PubSubUri class

This commit is contained in:
Paul Schaub 2020-08-16 15:14:36 +02:00
parent 07ce987d20
commit fd78deedb1
Signed by: vanitasvitae
GPG Key ID: 62BEE9264BF17311
2 changed files with 370 additions and 0 deletions

View File

@ -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 <a href="https://tools.ietf.org/html/rfc5122#section-2.2">ABNF in section 2.2 of rfc5122</a>
* @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 <a href="https://xmpp.org/extensions/xep-0060.html#registrar-querytypes">Registered URI Query Types</a>
*/
public enum Action {
subscribe,
unsubscribe,
retrieve
}
}

View File

@ -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 <a href="https://xmpp.org/extensions/xep-0060.html#example-227">
* XEP-0060: Publish-Subscribe: Example 227. XMPP URI for a node</a>
* @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 <a href="https://xmpp.org/extensions/xep-0060.html#example-228">
* XEP-0060: Publish-Subscribe: Example 228. XMPP URI for a pubsub item</a>
* @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 <a href="https://xmpp.org/extensions/xep-0060.html#example-229">
* XEP-0060: Publish-Subscribe: Example 229. URI for subscribing to a pubsub node</a>
* @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 <a href="https://xmpp.org/extensions/xep-0060.html#example-230">
* XEP-0060: Publish-Subscribe: Example 230. URI for retrieving a pubsub item</a>
* @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 <a href="https://xmpp.org/extensions/xep-0060.html#example-233">
* XEP-0060: Publish-Subscribe: Example 233. Pubsub Unsubscribe Action: IRI/URI</a>
* @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 <a href="https://xmpp.org/extensions/xep-0060.html#example-235">
* XEP-0060: Publish-Subscribe: Example 235. Pubsub Retrieve Action: IRI/URI</a>
*/
@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"));
}
}