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; } public PubSubUri(URI uri) throws XmppStringprepException { throwIfNoXmppUri(uri); String ssp = uri.getSchemeSpecificPart(); service = getService(ssp); node = getNodeOrNull(ssp); item = getItemOrNull(ssp); action = getActionOrNull(ssp); } /** * 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); return new PubSubUri(uri); } /** * 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 } }