/* * Copyright (c) 2010-2016, Isode Limited, London, England. * All rights reserved. */ /* * Copyright (c) 2010, Remko Tronçon. * All rights reserved. */ /* * Copyright (c) 2015 Tarun Gupta. * Licensed under the simplified BSD license. * See Documentation/Licenses/BSD-simplified.txt for more information. */ package com.isode.stroke.jid; import java.util.Arrays; import java.util.List; import com.isode.stroke.base.NotNull; import com.isode.stroke.idn.ICUConverter; import com.isode.stroke.idn.IDNConverter; /** * JID helper. * * This represents the JID used in XMPP * (RFC6120 - http://tools.ietf.org/html/rfc6120 particularly section 1.4), * further defined in XMPP Address Format (http://tools.ietf.org/html/rfc6122 ). * For a description of format, see the RFC or page 14 of * XMPP: The Definitive Guide (Saint-Andre et al.) * * Particularly - a Bare JID is a JID without a resource part. * * Note that invalid JIDs shouldn't have any calls made to them beyond isValid(). * * JIDs may be invalid if received over the wire, and should be checked with {@link JID#isValid} * before they're used. * *

* This is an immutable class. */ public class JID implements Comparable { public enum CompareType { WithResource, WithoutResource }; private boolean valid_ = true; private String node_ = ""; private String domain_ = ""; private String resource_ = null; private static IDNConverter idnConverter = new ICUConverter(); /** * Create an invalid JID. */ public JID() { this("", "", null); } /** * Create a JID using the JID(String) constructor. * @param jid String formatted JID, not null * @return Jabber ID, not null */ public static JID fromString(String jid) { NotNull.exceptIfNull(jid, "jid"); return new JID(jid); } /** * Create a JID from its String representation. * * e.g. * wonderland.lit * wonderland.lit/rabbithole * alice@wonderland.lit * alice@wonderland.lit/TeaParty * * @param jid String representation. Invalid JID if null or invalid. */ public JID(String jid) { NotNull.exceptIfNull(jid, "jid"); initializeFromString(jid); } private void initializeFromString(String jid) { if (jid.startsWith("@")) { valid_ = false; node_ = ""; domain_ = ""; resource_ = null; return; } String bare; String resource; String[] parts = jid.split("/", 2); if (parts.length > 1) { bare = parts[0]; resource = parts[1]; } else { resource = null; bare = jid; } String[] nodeAndDomain = bare.split("@", 2); String node; String domain; if (nodeAndDomain.length == 1) { node = ""; domain = nodeAndDomain[0]; } else { node = nodeAndDomain[0]; domain = nodeAndDomain[1]; } nameprepAndSetComponents(node, domain, resource); } /** * Create a bare JID from the node and domain parts. * * JID("node@domain") == JID("node", "domain") * Use a different constructor instead of passing nulls. * * @param node JID node part. * @param domain JID domain part. */ public JID(String node, String domain) { this(node, domain, null); } /** * Create a bare JID from the node, domain and resource parts. * * JID("node@domain/resource") == JID("node", "domain", "resource") * Use a different constructor instead of passing nulls. * * @param node JID node part. * @param domain JID domain part. * @param resource JID resource part. */ public JID(String node, String domain, String resource) { NotNull.exceptIfNull(node, "node"); NotNull.exceptIfNull(domain, "domain"); nameprepAndSetComponents(node, domain, resource); } private void nameprepAndSetComponents(String node, String domain, String resource) { if (domain.isEmpty() || idnConverter.getIDNAEncoded(domain) == null) { valid_ = false; return; } try { node_ = idnConverter.getStringPrepared(node, IDNConverter.StringPrepProfile.XMPPNodePrep); domain_ = idnConverter.getStringPrepared(domain, IDNConverter.StringPrepProfile.NamePrep); resource_ = resource != null ? idnConverter.getStringPrepared(resource, IDNConverter.StringPrepProfile.XMPPResourcePrep) : null; } catch (IllegalArgumentException e) { valid_ = false; return; } if (resource_ != null && resource_.isEmpty()) { valid_ = false; } if (domain_.isEmpty()) { valid_ = false; return; } } /** * @return Is a correctly-formatted JID. */ public boolean isValid() { return valid_; } /** * e.g. JID("node@domain").getNode() == "node" * @return Node, or null for nodeless JIDs. */ public String getNode() { return node_; } /** * e.g. JID("node@domain").getDomain() == "domain" * @return only null for invalid JIDs. */ public String getDomain() { return domain_; } /** * e.g. JID("node@domain/resource").getResource() == "resource" * @return "" for bare JIDs. */ public String getResource() { return resource_ != null ? resource_ : ""; } /** * Is a bare JID, i.e. has no resource part. * @return true if the resource part of JID is null, false if not */ public boolean isBare() { return resource_ == null; } /** * Get the JID without a resource. * @return non-null. Invalid if the original is invalid. */ public JID toBare() { return new JID(getNode(), getDomain()); } static List escapedChars = Arrays.asList(' ', '"', '&', '\'', '/', '<', '>', '@', ':'); private static String getEscaped(char c) { return "\\" + Integer.toHexString((int)c); } private static boolean getEscapeSequenceValue(String sequence) { try { int v = Integer.parseInt(sequence, 16); char value = (char)(v); return ((value == 0x5C) || escapedChars.contains(value)); } catch (NumberFormatException e) { return false; } } /** * Returns the given node, escaped according to XEP-0106. * The resulting node is a valid node for a JID, whereas the input value can contain characters * that are not allowed. */ public static String getEscapedNode(String node) { String result = ""; int startIndex = 0; for(char i : node.toCharArray()) { if(escapedChars.contains(i)) { result += getEscaped(i); continue; } else if (i == '\\') { int index = node.indexOf(i, startIndex); startIndex = index + 1; // Check if we have an escaped dissalowed character sequence int innerBegin = index + 1; if(innerBegin < node.length() && (innerBegin + 1) < node.length()) { int innerEnd = innerBegin + 2; String subs = node.substring(innerBegin, innerEnd); if(getEscapeSequenceValue(subs)) { result += getEscaped(i); continue; } } } result += i; } return result; } /** * Returns the node of the current JID, unescaped according to XEP-0106. */ public String getUnescapedNode() { String result = ""; int len = node_.length(); for(int j = 0; j != len;) { if (node_.charAt(j) == '\\') { int innerEnd = j + 1; for (int i = 0; i < 2 && innerEnd != node_.length(); ++i, ++innerEnd) { } char value; String subs = node_.substring(j+1, innerEnd); if (getEscapeSequenceValue(subs)) { int x = Integer.parseInt(subs, 16); value = (char)(x); result += value; j = innerEnd; continue; } } result += node_.charAt(j); ++j; } return result; } /** * Get the full JID with the supplied resource. */ public JID withResource(String resource) { return new JID (this.getNode(), this.getDomain(), resource); } public void setIDNConverter(IDNConverter converter) { idnConverter = converter; } @Override public String toString() { String string = new String(); if (node_.length()!=0) { string += node_ + "@"; } string += domain_; if (!isBare()) { string += "/" + resource_; } return string; } @Override public boolean equals(Object otherObject) { if (!(otherObject instanceof JID)) { return false; } return (compare((JID)otherObject, CompareType.WithResource) == 0); } /** * Compare two Jabber IDs by either including the resource part or * excluding it * @param o other JID to which this JID should be compared, can be null * @param compareType comparison type * @return 0 if equal, 1 if other JID is greater or -1 if this JID is greater */ public int compare(JID o, CompareType compareType) { if(this == o) return 0; if(o == null) return 1; if (!node_ .equals(o.node_)) { return node_.compareTo(o.node_); } if (!domain_ .equals(o.domain_)) { return domain_.compareTo(o.domain_); } if (compareType == CompareType.WithResource) { if (isBare() != o.isBare()) { return !isBare() ? 1 : -1; } if (!isBare()) { return resource_.compareTo(o.resource_); } } return 0; } @Override public int hashCode() { int hash = 5; hash = 73 * hash + (this.node_ != null ? this.node_.hashCode() : 0); hash = 73 * hash + (this.domain_ != null ? this.domain_.hashCode() : 0); hash = 73 * hash + (this.resource_ != null ? this.resource_.hashCode() : 0); return hash; } @Override public int compareTo(JID o) { return compare(o, CompareType.WithResource); } /** * Returns a JID object corresponding to the given * String, or null if the String does not represent a valid JID * @param s value to be parsed * @return a JID, or null if s does not parse as a valid JID */ public JID parse(String s) { JID jid = new JID(s); return jid.isValid() ? jid : null; } }