rocks.xmpp.addr.Jid Maven / Gradle / Ivy
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2016 Christian Schudt
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package rocks.xmpp.addr;
import rocks.xmpp.precis.PrecisProfile;
import rocks.xmpp.precis.PrecisProfiles;
import rocks.xmpp.util.cache.LruCache;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import java.io.Serializable;
import java.net.IDN;
import java.nio.charset.StandardCharsets;
import java.text.Normalizer;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* The implementation of the JID as described in Extensible Messaging and Presence Protocol (XMPP): Address Format.
*
* A JID consists of three parts:
*
* [ localpart "@" ] domainpart [ "/" resourcepart ]
*
* The easiest way to create a JID is to use the {@link #of(CharSequence)} method:
*
* Jid jid = Jid.of("[email protected]/balcony");
*
* You can then get the parts from it via the respective methods:
*
* String local = jid.getLocal(); // juliet
* String domain = jid.getDomain(); // capulet.lit
* String resource = jid.getResource(); // balcony
*
* This class overrides equals()
and hashCode()
, so that different instances with the same value are equal:
*
* Jid.of("[email protected]/balcony").equals(Jid.of("[email protected]/balcony")); // true
*
* This class also supports XEP-0106: JID Escaping, i.e.
*
* Jid.of("d'[email protected]")
*
* is escaped as d\\[email protected]
.
*
* This class is thread-safe and immutable.
*
* @author Christian Schudt
* @see RFC 7622 - Extensible Messaging and Presence Protocol (XMPP): Address Format
*/
@XmlJavaTypeAdapter(JidAdapter.class)
public final class Jid implements Comparable, Serializable, CharSequence {
/**
* The service discovery feature used for determining support of JID escaping (jid\20escaping
).
*/
public static final String ESCAPING_FEATURE = "jid\\20escaping";
/**
* Escapes all disallowed characters and also backslash, when followed by a defined hex code for escaping. See 4. Business Rules.
*/
private static final Pattern ESCAPE_PATTERN = Pattern.compile("[ \"&'/:<>@]|\\\\(?=20|22|26|27|2f|3a|3c|3e|40|5c)");
private static final Pattern UNESCAPE_PATTERN = Pattern.compile("\\\\(20|22|26|27|2f|3a|3c|3e|40|5c)");
private static final String DOMAIN_PART = "((?:(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9]))+)";
private static final Pattern JID = Pattern.compile("^((.*?)@)?" + DOMAIN_PART + "(/(.*))?$");
private static final IDNProfile IDN_PROFILE = new IDNProfile();
/**
* Label separators for domain labels, which should be mapped to "." (dot): IDEOGRAPHIC FULL STOP character (U+3002)
*/
private static final Pattern LABEL_SEPARATOR = Pattern.compile("\u3002");
/**
* Caches the escaped JIDs.
*/
private static final Map ESCAPED_CACHE = new LruCache<>(5000);
/**
* Caches the unescaped JIDs.
*/
private static final Map UNESCAPED_CACHE = new LruCache<>(5000);
private static final long serialVersionUID = -3824234106101731424L;
private final String escapedLocal;
private final String local;
private final String domain;
private final String resource;
/**
* Creates a full JID with local, domain and resource part.
*
* @param local The local part.
* @param domain The domain part.
* @param resource The resource part.
*/
private Jid(CharSequence local, CharSequence domain, CharSequence resource) {
this(local, domain, resource, false, true);
}
private Jid(final CharSequence local, final CharSequence domain, final CharSequence resource, final boolean doUnescape, final boolean enforceAndValidate) {
final String enforcedLocalPart;
final String enforcedDomainPart;
final String enforcedResource;
// If the domainpart includes a final character considered to be a label
// separator (dot) by [RFC1034], this character MUST be stripped from
// the domainpart before the JID of which it is a part is used for the
// purpose of routing an XML stanza, comparing against another JID, or
// constructing an XMPP URI or IRI [RFC5122]. In particular, such a
// character MUST be stripped before any other canonicalization steps
// are taken.
final String strDomain = Objects.requireNonNull(domain).toString().replaceAll("\\.$", "");
final String unescapedLocalPart;
if (doUnescape) {
unescapedLocalPart = unescape(local);
} else {
unescapedLocalPart = local != null ? local.toString() : null;
}
// Escape the local part, so that disallowed characters like the space characters pass the UsernameCaseMapped profile.
final String escapedLocalPart = escape(unescapedLocalPart);
if (enforceAndValidate) {
enforcedLocalPart = escapedLocalPart != null ? PrecisProfiles.USERNAME_CASE_MAPPED.enforce(escapedLocalPart) : null;
enforcedResource = resource != null ? PrecisProfiles.OPAQUE_STRING.enforce(resource) : null;
// See https://tools.ietf.org/html/rfc5895#section-2
enforcedDomainPart = IDN_PROFILE.enforce(strDomain);
validateLength(enforcedLocalPart, "local");
validateLength(enforcedResource, "resource");
validateDomain(strDomain);
} else {
enforcedLocalPart = escapedLocalPart != null ? escapedLocalPart : null;
enforcedResource = resource != null ? resource.toString() : null;
enforcedDomainPart = strDomain;
}
this.local = unescape(enforcedLocalPart);
this.escapedLocal = enforcedLocalPart;
this.domain = enforcedDomainPart;
this.resource = enforcedResource;
}
/**
* Returns a full JID with a domain and resource part, e.g. capulet.com/balcony
*
* @param local The local part.
* @param domain The domain.
* @param resource The resource part.
* @return The JID.
* @throws NullPointerException If the domain is null.
* @throws IllegalArgumentException If the domain, local or resource part are not valid.
*/
public static Jid of(CharSequence local, CharSequence domain, CharSequence resource) {
return new Jid(local, domain, resource);
}
/**
* Creates a bare JID with only the domain part, e.g. capulet.com
*
* @param domain The domain.
* @return The JID.
* @throws NullPointerException If the domain is null.
* @throws IllegalArgumentException If the domain or local part are not valid.
*/
public static Jid ofDomain(CharSequence domain) {
return new Jid(null, domain, null);
}
/**
* Creates a bare JID with a local and domain part, e.g. [email protected]
*
* @param local The local part.
* @param domain The domain.
* @return The JID.
* @throws NullPointerException If the domain is null.
* @throws IllegalArgumentException If the domain or local part are not valid.
*/
public static Jid ofLocalAndDomain(CharSequence local, CharSequence domain) {
return new Jid(local, domain, null);
}
/**
* Creates a full JID with a domain and resource part, e.g. capulet.com/balcony
*
* @param domain The domain.
* @param resource The resource part.
* @return The JID.
* @throws NullPointerException If the domain is null.
* @throws IllegalArgumentException If the domain or resource are not valid.
*/
public static Jid ofDomainAndResource(CharSequence domain, CharSequence resource) {
return new Jid(null, domain, resource);
}
/**
* Creates a JID from an unescaped string. The format must be
* [ localpart "@" ] domainpart [ "/" resourcepart ]
* The input string will be escaped.
*
* @param jid The JID.
* @return The JID.
* @throws NullPointerException If the jid is null.
* @throws IllegalArgumentException If the jid could not be parsed or is not valid.
* @see XEP-0106: JID Escaping
*/
public static Jid of(CharSequence jid) {
return of(jid.toString(), false, true);
}
/**
* Creates a JID from a escaped JID string. The format must be
* [ localpart "@" ] domainpart [ "/" resourcepart ]
* This method should be used, when parsing JIDs from the XMPP stream.
*
* Note, that validation and enforcement is skipped when using this method, because it's expected,
* that JID strings passed to this method are already enforced.
*
* @param jid The JID.
* @return The JID.
* @throws NullPointerException If the jid is null.
* @throws IllegalArgumentException If the jid could not be parsed or is not valid.
* @see XEP-0106: JID Escaping
*/
public static Jid ofEscaped(CharSequence jid) {
return of(jid.toString(), true, false);
}
/**
* Creates a JID from a string. The format must be
*
[ localpart "@" ] domainpart [ "/" resourcepart ]
*
* @param jid The JID.
* @param doUnescape If the jid parameter will be unescaped.
* @param enforceAndValidate If the JID should be enforced and validated.
* @return The JID.
* @throws NullPointerException If the jid is null.
* @throws IllegalArgumentException If the jid could not be parsed or is not valid.
* @see XEP-0106: JID Escaping
*/
private static Jid of(String jid, final boolean doUnescape, final boolean enforceAndValidate) {
Objects.requireNonNull(jid, "jid must not be null.");
jid = jid.trim();
if (jid.isEmpty()) {
throw new IllegalArgumentException("jid must not be empty.");
}
Jid result;
if (doUnescape) {
result = UNESCAPED_CACHE.get(jid);
} else {
result = ESCAPED_CACHE.get(jid);
}
if (result != null) {
return result;
}
Matcher matcher = JID.matcher(jid);
if (matcher.matches()) {
Jid jidValue = new Jid(matcher.group(2), matcher.group(3), matcher.group(8), doUnescape, enforceAndValidate);
if (doUnescape) {
UNESCAPED_CACHE.put(jid, jidValue);
} else {
ESCAPED_CACHE.put(jid, jidValue);
}
return jidValue;
} else {
throw new IllegalArgumentException("Could not parse JID: " + jid);
}
}
/**
* Escapes a local part. The characters {@code "&'/:<>@} (+ whitespace) are replaced with their respective escape characters.
*
* @param localPart The local part.
* @return The escaped local part or null.
* @see XEP-0106: JID Escaping
*/
private static String escape(CharSequence localPart) {
if (localPart != null) {
Matcher matcher = ESCAPE_PATTERN.matcher(localPart);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
String match = matcher.group();
matcher.appendReplacement(sb, String.format("\\\\%x", match.getBytes(StandardCharsets.UTF_8)[0]));
}
matcher.appendTail(sb);
return sb.toString();
}
return null;
}
private static String unescape(CharSequence localPart) {
if (localPart != null) {
Matcher matcher = UNESCAPE_PATTERN.matcher(localPart);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
String match = matcher.group(1);
int num = Integer.parseInt(match, 16);
String value = String.valueOf((char) num);
if (value.equals("\\")) {
matcher.appendReplacement(sb, "\\\\");
} else {
matcher.appendReplacement(sb, value);
}
}
matcher.appendTail(sb);
return sb.toString();
}
return null;
}
private static void validateDomain(String domain) {
Objects.requireNonNull(domain, "domain must not be null.");
if (domain.contains("@")) {
// Prevent misuse of API.
throw new IllegalArgumentException("domain must not contain a '@' sign");
}
validateLength(domain, "domain");
}
/**
* Validates that the length of a local, domain or resource part is not longer than 1023 characters.
*
* @param value The value.
* @param part The part, only used to produce an exception message.
*/
private static void validateLength(CharSequence value, CharSequence part) {
if (value != null) {
if (value.length() == 0) {
throw new IllegalArgumentException(part + " must not be empty.");
}
if (value.length() > 1023) {
throw new IllegalArgumentException(part + " must not be greater than 1023 characters.");
}
}
}
/**
* Checks if the JID is a full JID.
*
* The term "full JID" refers to an XMPP address of the form <localpart@domainpart/resourcepart> (for a particular authorized client or device associated with an account) or of the form <domainpart/resourcepart> (for a particular resource or script associated with a server).
*
*
* @return True, if the JID is a full JID; otherwise false.
*/
public final boolean isFullJid() {
return resource != null;
}
/**
* Checks if the JID is a bare JID.
*
* The term "bare JID" refers to an XMPP address of the form <localpart@domainpart> (for an account at a server) or of the form <domainpart> (for a server).
*
*
* @return True, if the JID is a bare JID; otherwise false.
*/
public final boolean isBareJid() {
return resource == null;
}
/**
* Converts this JID into a bare JID, i.e. removes the resource part.
*
* The term "bare JID" refers to an XMPP address of the form <localpart@domainpart> (for an account at a server) or of the form <domainpart> (for a server).
*
*
* @return The bare JID.
* @see #withResource(CharSequence)
*/
public final Jid asBareJid() {
return new Jid(local, domain, null, false, false);
}
/**
* Creates a new JID with a new local part and the same domain and resource part of the current JID.
*
* @param local The local part.
* @return The JID with a new local part.
* @throws IllegalArgumentException If the local is not a valid local part.
* @see #withResource(CharSequence)
*/
public final Jid withLocal(CharSequence local) {
return new Jid(local, domain, resource, false, true);
}
/**
* Creates a new full JID with a resource and the same local and domain part of the current JID.
*
* @param resource The resource.
* @return The full JID with a resource.
* @throws IllegalArgumentException If the resource is not a valid resource part.
* @see #asBareJid()
* @see #withLocal(CharSequence)
*/
public final Jid withResource(CharSequence resource) {
return new Jid(local, domain, resource, false, true);
}
/**
* Creates a new JID at a subdomain and at the same domain as this JID.
*
* @param subdomain The subdomain.
* @return The JID at a subdomain.
* @throws NullPointerException If subdomain is null.
* @throws IllegalArgumentException If subdomain is not a valid subdomain name.
*/
public final Jid atSubdomain(CharSequence subdomain) {
return new Jid(local, Objects.requireNonNull(subdomain) + "." + domain, resource, false, true);
}
/**
* Gets the local part of the JID, also known as the name or node.
*
*
* The localpart of a JID is an optional identifier placed before the
* domainpart and separated from the latter by the '@' character.
* Typically, a localpart uniquely identifies the entity requesting and
* using network access provided by a server (i.e., a local account),
* although it can also represent other kinds of entities (e.g., a
* chatroom associated with a multi-user chat service [XEP-0045]). The
* entity represented by an XMPP localpart is addressed within the
* context of a specific domain (i.e., <localpart@domainpart>).
*
*
* @return The local part or null.
*/
public final String getLocal() {
return local;
}
/**
* Gets the domain part.
*
*
* The domainpart is the primary identifier and is the only REQUIRED
* element of a JID (a mere domainpart is a valid JID). Typically,
* a domainpart identifies the "home" server to which clients connect
* for XML routing and data management functionality.
*
*
* @return The domain part.
*/
public final String getDomain() {
return domain;
}
/**
* Gets the resource part.
*
*
* The resourcepart of a JID is an optional identifier placed after the
* domainpart and separated from the latter by the '/' character. A
* resourcepart can modify either a <localpart@domainpart> address or a
* mere <domainpart> address. Typically, a resourcepart uniquely
* identifies a specific connection (e.g., a device or location) or
* object (e.g., an occupant in a multi-user chatroom [XEP-0045])
* belonging to the entity associated with an XMPP localpart at a domain
* (i.e., <localpart@domainpart/resourcepart>).
*
*
* @return The resource part or null.
*/
public final String getResource() {
return resource;
}
/**
* Returns the JID in escaped form as described in XEP-0106: JID Escaping.
*
* @return The escaped JID.
* @see #toString()
*/
public final String toEscapedString() {
return toString(escapedLocal, domain, resource);
}
@Override
public final int length() {
return toString().length();
}
@Override
public final char charAt(int index) {
return toString().charAt(index);
}
@Override
public final CharSequence subSequence(int start, int end) {
return toString().subSequence(start, end);
}
/**
* Returns the JID in its string representation, i.e. [ localpart "@" ] domainpart [ "/" resourcepart ].
*
* @return The JID.
* @see #toEscapedString()
*/
@Override
public final String toString() {
return toString(local, domain, resource);
}
private static String toString(String local, String domain, String resource) {
StringBuilder sb = new StringBuilder();
if (local != null) {
sb.append(local).append('@');
}
sb.append(domain);
if (resource != null) {
sb.append('/').append(resource);
}
return sb.toString();
}
@Override
public final boolean equals(Object o) {
if (o == this) {
return true;
}
if (!(o instanceof Jid)) {
return false;
}
Jid other = (Jid) o;
return Objects.equals(local, other.local)
&& Objects.equals(domain, other.domain)
&& Objects.equals(resource, other.resource);
}
@Override
public final int hashCode() {
return Objects.hash(local, domain, resource);
}
/**
* Compares this JID with another JID. First domain parts are compared. If these are equal, local parts are compared
* and if these are equal, too, resource parts are compared.
*
* @param o The other JID.
* @return The comparison result.
*/
@Override
public final int compareTo(Jid o) {
if (this == o) {
return 0;
}
if (o != null) {
int result;
// First compare domain parts.
if (domain != null) {
result = o.domain != null ? domain.compareTo(o.domain) : -1;
} else {
result = o.domain != null ? 1 : 0;
}
// If the domains are equal, compare local parts.
if (result == 0) {
if (local != null) {
// If this local part is not null, but the other is null, move this down (1).
result = o.local != null ? local.compareTo(o.local) : 1;
} else {
// If this local part is null, but the other is not, move this up (-1).
result = o.local != null ? -1 : 0;
}
}
// If the local parts are equal, compare resource parts.
if (result == 0) {
if (resource != null) {
// If this resource part is not null, but the other is null, move this down (1).
return o.resource != null ? resource.compareTo(o.resource) : 1;
} else {
// If this resource part is null, but the other is not, move this up (-1).
return o.resource != null ? -1 : 0;
}
}
return result;
} else {
return -1;
}
}
/**
* A profile for applying the rules for IDN as in RFC 5895. Although IDN doesn't use Precis, it's still very similar so that we can use the base class.
*
* @see RFC 5895
*/
private static final class IDNProfile extends PrecisProfile {
private IDNProfile() {
super(false);
}
@Override
public String prepare(CharSequence input) {
return IDN.toUnicode(input.toString(), IDN.USE_STD3_ASCII_RULES);
}
@Override
public String enforce(CharSequence input) {
// 4. Map IDEOGRAPHIC FULL STOP character (U+3002) to dot.
return applyAdditionalMappingRule(
// 3. All characters are mapped using Unicode Normalization Form C (NFC).
applyNormalizationRule(
// 2. Fullwidth and halfwidth characters (those defined with
// Decomposition Types and ) are mapped to their
// decomposition mappings
applyWidthMappingRule(
// 1. Uppercase characters are mapped to their lowercase equivalents
applyCaseMappingRule(prepare(input))))).toString();
}
@Override
protected CharSequence applyWidthMappingRule(CharSequence charSequence) {
return widthMap(charSequence);
}
@Override
protected CharSequence applyAdditionalMappingRule(CharSequence charSequence) {
return LABEL_SEPARATOR.matcher(charSequence).replaceAll(".");
}
@Override
protected CharSequence applyCaseMappingRule(CharSequence charSequence) {
return charSequence.toString().toLowerCase();
}
@Override
protected CharSequence applyNormalizationRule(CharSequence charSequence) {
return Normalizer.normalize(charSequence, Normalizer.Form.NFC);
}
@Override
protected CharSequence applyDirectionalityRule(CharSequence charSequence) {
return charSequence;
}
}
}