JID.java 19.7 KB
Newer Older
1
/**
Gaston Dombiak's avatar
Gaston Dombiak committed
2
 * Copyright (C) 2004-2007 Jive Software. All rights reserved.
3
 *
Gaston Dombiak's avatar
Gaston Dombiak committed
4 5
 * This software is published under the terms of the GNU Public License (GPL),
 * a copy of which is included in this distribution.
6 7 8 9 10 11
 */

package org.xmpp.packet;

import org.jivesoftware.stringprep.IDNA;
import org.jivesoftware.stringprep.Stringprep;
12
import org.jivesoftware.stringprep.StringprepException;
Gaston Dombiak's avatar
Gaston Dombiak committed
13
import org.jivesoftware.util.cache.ExternalizableUtil;
14

Gaston Dombiak's avatar
Gaston Dombiak committed
15
import java.io.*;
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * An XMPP address (JID). A JID is made up of a node (generally a username), a domain,
 * and a resource. The node and resource are optional; domain is required. In simple
 * ABNF form:
 *
 * <ul><tt>jid = [ node "@" ] domain [ "/" resource ]</tt></ul>
 *
 * Some sample JID's:
 * <ul>
 *      <li><tt>user@example.com</tt></li>
 *      <li><tt>user@example.com/home</tt></li>
 *      <li><tt>example.com</tt></li>
 * </ul>
 *
 * Each allowable portion of a JID (node, domain, and resource) must not be more
 * than 1023 bytes in length, resulting in a maximum total size (including the '@'
 * and '/' separators) of 3071 bytes.
 *
 * @author Matt Tucker
 */
Gaston Dombiak's avatar
Gaston Dombiak committed
40
public class JID implements Comparable<JID>, Serializable, Externalizable {
41 42 43 44

    // Stringprep operations are very expensive. Therefore, we cache node, domain and
    // resource values that have already had stringprep applied so that we can check
    // incoming values against the cache.
Gaston Dombiak's avatar
Gaston Dombiak committed
45
    private static Map<String,Object> stringprepCache = Collections.synchronizedMap(new Cache(10000));
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194

    private String node;
    private String domain;
    private String resource;

    private String cachedFullJID;
    private String cachedBareJID;

    /**
     * Escapes the node portion of a JID according to "JID Escaping" (JEP-0106).
     * Escaping replaces characters prohibited by node-prep with escape sequences,
     * as follows:<p>
     *
     * <table border="1">
     * <tr><td><b>Unescaped Character</b></td><td><b>Encoded Sequence</b></td></tr>
     * <tr><td>&lt;space&gt;</td><td>\20</td></tr>
     * <tr><td>"</td><td>\22</td></tr>
     * <tr><td>&</td><td>\26</td></tr>
     * <tr><td>'</td><td>\27</td></tr>
     * <tr><td>/</td><td>\2f</td></tr>
     * <tr><td>:</td><td>\3a</td></tr>
     * <tr><td>&lt;</td><td>\3c</td></tr>
     * <tr><td>&gt;</td><td>\3e</td></tr>
     * <tr><td>@</td><td>\40</td></tr>
     * <tr><td>\</td><td>\5c</td></tr>
     * </table><p>
     *
     * This process is useful when the node comes from an external source that doesn't
     * conform to nodeprep. For example, a username in LDAP may be "Joe Smith". Because
     * the &lt;space&gt; character isn't a valid part of a node, the username should
     * be escaped to "Joe\20Smith" before being made into a JID (e.g. "joe\20smith@example.com"
     * after case-folding, etc. has been applied).<p>
     *
     * All node escaping and un-escaping must be performed manually at the appropriate
     * time; the JID class will not escape or un-escape automatically.
     *
     * @param node the node.
     * @return the escaped version of the node.
     */
    public static String escapeNode(String node) {
        if (node == null) {
            return null;
        }
        StringBuilder buf = new StringBuilder(node.length() + 8);
        for (int i=0, n=node.length(); i<n; i++) {
            char c = node.charAt(i);
            switch (c) {
                case '"': buf.append("\\22"); break;
                case '&': buf.append("\\26"); break;
                case '\'': buf.append("\\27"); break;
                case '/': buf.append("\\2f"); break;
                case ':': buf.append("\\3a"); break;
                case '<': buf.append("\\3c"); break;
                case '>': buf.append("\\3e"); break;
                case '@': buf.append("\\40"); break;
                case '\\': buf.append("\\5c"); break;
                default: {
                    if (Character.isWhitespace(c)) {
                        buf.append("\\20");
                    }
                    else {
                        buf.append(c);
                    }
                }
            }
        }
        return buf.toString();
    }

    /**
     * Un-escapes the node portion of a JID according to "JID Escaping" (JEP-0106).<p>
     * Escaping replaces characters prohibited by node-prep with escape sequences,
     * as follows:<p>
     *
     * <table border="1">
     * <tr><td><b>Unescaped Character</b></td><td><b>Encoded Sequence</b></td></tr>
     * <tr><td>&lt;space&gt;</td><td>\20</td></tr>
     * <tr><td>"</td><td>\22</td></tr>
     * <tr><td>&</td><td>\26</td></tr>
     * <tr><td>'</td><td>\27</td></tr>
     * <tr><td>/</td><td>\2f</td></tr>
     * <tr><td>:</td><td>\3a</td></tr>
     * <tr><td>&lt;</td><td>\3c</td></tr>
     * <tr><td>&gt;</td><td>\3e</td></tr>
     * <tr><td>@</td><td>\40</td></tr>
     * <tr><td>\</td><td>\5c</td></tr>
     * </table><p>
     *
     * This process is useful when the node comes from an external source that doesn't
     * conform to nodeprep. For example, a username in LDAP may be "Joe Smith". Because
     * the &lt;space&gt; character isn't a valid part of a node, the username should
     * be escaped to "Joe\20Smith" before being made into a JID (e.g. "joe\20smith@example.com"
     * after case-folding, etc. has been applied).<p>
     *
     * All node escaping and un-escaping must be performed manually at the appropriate
     * time; the JID class will not escape or un-escape automatically.
     *
     * @param node the escaped version of the node.
     * @return the un-escaped version of the node.
     */
    public static String unescapeNode(String node) {
        if (node == null) {
            return null;
        }
        char [] nodeChars = node.toCharArray();
        StringBuilder buf = new StringBuilder(nodeChars.length);
        for (int i=0, n=nodeChars.length; i<n; i++) {
            compare: {
                char c = node.charAt(i);
                if (c == '\\' && i+2<n) {
                    char c2 = nodeChars[i+1];
                    char c3 = nodeChars[i+2];
                    if (c2 == '2') {
                        switch (c3) {
                            case '0': buf.append(' '); i+=2; break compare;
                            case '2': buf.append('"'); i+=2; break compare;
                            case '6': buf.append('&'); i+=2; break compare;
                            case '7': buf.append('\''); i+=2; break compare;
                            case 'f': buf.append('/'); i+=2; break compare;
                        }
                    }
                    else if (c2 == '3') {
                        switch (c3) {
                            case 'a': buf.append(':'); i+=2; break compare;
                            case 'c': buf.append('<'); i+=2; break compare;
                            case 'e': buf.append('>'); i+=2; break compare;
                        }
                    }
                    else if (c2 == '4') {
                        if (c3 == '0') {
                            buf.append("@");
                            i+=2;
                            break compare;
                        }
                    }
                    else if (c2 == '5') {
                        if (c3 == 'c') {
                            buf.append("\\");
                            i+=2;
                            break compare;
                        }
                    }
                }
                buf.append(c);
            }
        }
        return buf.toString();
    }

195 196 197 198 199 200 201 202 203 204 205 206 207
    public static String resourceprep(String resource) throws StringprepException {
        String answer = resource;
        if (!stringprepCache.containsKey(resource)) {
            answer = Stringprep.resourceprep(resource);
            // Validate field is not greater than 1023 bytes. UTF-8 characters use two bytes.
            if (answer != null && answer.length()*2 > 1023) {
                return answer;
            }
            stringprepCache.put(answer, null);
        }
        return answer;
    }

Gaston Dombiak's avatar
Gaston Dombiak committed
208 209 210 211 212 213
    /**
     * Constructor added for Externalizable. Do not use this constructor.
     */
    public JID() {
    }

214 215 216 217 218 219 220 221 222 223
    /**
     * Constructs a JID from it's String representation.
     *
     * @param jid a valid JID.
     * @throws IllegalArgumentException if the JID is not valid.
     */
    public JID(String jid) {
        if (jid == null) {
            throw new NullPointerException("JID cannot be null");
        }
224
        String[] parts = getParts(jid);
225

226
        init(parts[0], parts[1], parts[2]);
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244
    }

    /**
     * Constructs a JID given a node, domain, and resource.
     *
     * @param node the node.
     * @param domain the domain, which must not be <tt>null</tt>.
     * @param resource the resource.
     * @throws IllegalArgumentException if the JID is not valid.
     */
    public JID(String node, String domain, String resource) {
        if (domain == null) {
            throw new NullPointerException("Domain cannot be null");
        }
        init(node, domain, resource);
    }

    /**
245 246
     * Constructs a JID given a node, domain, and resource being able to specify if stringprep
     * should be applied or not.
247
     *
248 249 250 251 252
     * @param node the node.
     * @param domain the domain, which must not be <tt>null</tt>.
     * @param resource the resource.
     * @param skipStringprep true if stringprep should not be applied.
     * @throws IllegalArgumentException if the JID is not valid.
253
     */
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279
    public JID(String node, String domain, String resource, boolean skipStringprep) {
        if (domain == null) {
            throw new NullPointerException("Domain cannot be null");
        }
        if (skipStringprep) {
            this.node = node;
            this.domain = domain;
            this.resource = resource;
            // Cache the bare and full JID String representation
            updateCache();
        }
        else {
            init(node, domain, resource);
        }
    }

    /**
     * Returns a String array with the parsed node, domain and resource.
     * No Stringprep is performed while parsing the textual representation.
     *
     * @param jid the textual JID representation.
     * @return a string array with the parsed node, domain and resource.
     */
    static String[] getParts(String jid) {
        String[] parts = new String[3];
        String node = null , domain, resource;
280
        if (jid == null) {
281
            return parts;
282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319
        }

        int atIndex = jid.indexOf("@");
        int slashIndex = jid.indexOf("/");

        // Node
        if (atIndex > 0) {
            node = jid.substring(0, atIndex);
        }

        // Domain
        if (atIndex + 1 > jid.length()) {
            throw new IllegalArgumentException("JID with empty domain not valid");
        }
        if (atIndex < 0) {
            if (slashIndex > 0) {
                domain = jid.substring(0, slashIndex);
            }
            else {
                domain = jid;
            }
        }
        else {
            if (slashIndex > 0) {
                domain = jid.substring(atIndex + 1, slashIndex);
            }
            else {
                domain = jid.substring(atIndex + 1);
            }
        }

        // Resource
        if (slashIndex + 1 > jid.length() || slashIndex < 0) {
            resource = null;
        }
        else {
            resource = jid.substring(slashIndex + 1);
        }
320 321 322 323
        parts[0] = node;
        parts[1] = domain;
        parts[2] = resource;
        return parts;
324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347
    }

    /**
     * Transforms the JID parts using the appropriate Stringprep profiles, then
     * validates them. If they are fully valid, the field values are saved, otherwise
     * an IllegalArgumentException is thrown.
     *
     * @param node the node.
     * @param domain the domain.
     * @param resource the resource.
     */
    private void init(String node, String domain, String resource) {
        // Set node and resource to null if they are the empty string.
        if (node != null && node.equals("")) {
            node = null;
        }
        if (resource != null && resource.equals("")) {
            resource = null;
        }
        // Stringprep (node prep, resourceprep, etc).
        try {
            if (!stringprepCache.containsKey(node)) {
                this.node = Stringprep.nodeprep(node);
                // Validate field is not greater than 1023 bytes. UTF-8 characters use two bytes.
348
                if (this.node != null && this.node.length()*2 > 1023) {
349
                    throw new IllegalArgumentException("Node cannot be larger than 1023 bytes. " +
Gaston Dombiak's avatar
Gaston Dombiak committed
350
                            "Size is " + (this.node.length() * 2) + " bytes.");
351 352 353 354 355 356 357 358 359 360 361 362 363
                }
                stringprepCache.put(this.node, null);
            }
            else {
                this.node = node;
            }
            // XMPP specifies that domains should be run through IDNA and
            // that they should be run through nameprep before doing any
            // comparisons. We always run the domain through nameprep to
            // make comparisons easier later.
            if (!stringprepCache.containsKey(domain)) {
                this.domain = Stringprep.nameprep(IDNA.toASCII(domain), false);
                // Validate field is not greater than 1023 bytes. UTF-8 characters use two bytes.
364
                if (this.domain.length()*2 > 1023) {
365
                    throw new IllegalArgumentException("Domain cannot be larger than 1023 bytes. " +
366
                            "Size is " + (this.domain.length() * 2) + " bytes.");
367 368 369 370 371 372
                }
                stringprepCache.put(this.domain, null);
            }
            else {
                this.domain = domain;
            }
373 374 375 376 377
            this.resource = resourceprep(resource);
            // Validate field is not greater than 1023 bytes. UTF-8 characters use two bytes.
            if (resource != null && resource.length()*2 > 1023) {
                throw new IllegalArgumentException("Resource cannot be larger than 1023 bytes. " +
                        "Size is " + (resource.length() * 2) + " bytes.");
378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499
            }
            // Cache the bare and full JID String representation
            updateCache();
        }
        catch (Exception e) {
            StringBuilder buf = new StringBuilder();
            if (node != null) {
                buf.append(node).append("@");
            }
            buf.append(domain);
            if (resource != null) {
                buf.append("/").append(resource);
            }
            throw new IllegalArgumentException("Illegal JID: " + buf.toString(), e);
        }
    }

    private void updateCache() {
        // Cache the bare JID
        StringBuilder buf = new StringBuilder(40);
        if (node != null) {
            buf.append(node).append("@");
        }
        buf.append(domain);
        cachedBareJID = buf.toString();

        // Cache the full JID
        if (resource != null) {
            buf.append("/").append(resource);
            cachedFullJID = buf.toString();
        }
        else {
            cachedFullJID = cachedBareJID;
        }
    }

    /**
     * Returns the node, or <tt>null</tt> if this JID does not contain node information.
     *
     * @return the node.
     */
    public String getNode() {
        return node;
    }

    /**
     * Returns the domain.
     *
     * @return the domain.
     */
    public String getDomain() {
        return domain;
    }

    /**
     * Returns the resource, or <tt>null</tt> if this JID does not contain resource information.
     *
     * @return the resource.
     */
    public String getResource() {
        return resource;
    }

    /**
     * Returns the String representation of the bare JID, which is the JID with
     * resource information removed.
     *
     * @return the bare JID.
     */
    public String toBareJID() {
        return cachedBareJID;
    }

    /**
     * Returns a String representation of the JID.
     *
     * @return a String representation of the JID.
     */
    public String toString() {
        return cachedFullJID;
    }

    public int hashCode() {
        return toString().hashCode();
    }

    public boolean equals(Object object) {
        if (!(object instanceof JID)) {
            return false;
        }
        if (this == object) {
            return true;
        }
        JID jid = (JID)object;
        // Node. If node isn't null, compare.
        if (node != null) {
            if (!node.equals(jid.node)) {
                return false;
            }
        }
        // Otherwise, jid.node must be null.
        else if (jid.node != null) {
            return false;
        }
        // Compare domain, which must be null.
        if (!domain.equals(jid.domain)) {
            return false;
        }
        // Resource. If resource isn't null, compare.
        if (resource != null) {
            if (!resource.equals(jid.resource)) {
                return false;
            }
        }
        // Otherwise, jid.resource must be null.
        else if (jid.resource != null) {
            return false;
        }
        // Passed all checks, so equal.
        return true;
    }

Gaston Dombiak's avatar
Gaston Dombiak committed
500
    public int compareTo(JID jid) {
501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534
        // Comparison order is domain, node, resource.
        int compare = domain.compareTo(jid.domain);
        if (compare == 0 && node != null && jid.node != null) {
            compare = node.compareTo(jid.node);
        }
        if (compare == 0 && resource != null && jid.resource != null) {
            compare = resource.compareTo(jid.resource);
        }
        return compare;
    }

    /**
     * Returns true if two JID's are equivalent. The JID components are compared using
     * the following rules:<ul>
     *      <li>Nodes are normalized using nodeprep (case insensitive).
     *      <li>Domains are normalized using IDNA and then nameprep (case insensitive).
     *      <li>Resources are normalized using resourceprep (case sensitive).</ul>
     *
     * These normalization rules ensure, for example, that
     * <tt>User@EXAMPLE.com/home</tt> is considered equal to <tt>user@example.com/home</tt>.
     *
     * @param jid1 a JID.
     * @param jid2 a JID.
     * @return true if the JIDs are equivalent; false otherwise.
     * @throws IllegalArgumentException if either JID is not valid.
     */
    public static boolean equals(String jid1, String jid2) {
        return new JID(jid1).equals(new JID(jid2));
    }

    /**
     * A simple cache class that extends LinkedHashMap. It uses an LRU policy to
     * keep the cache at a maximum size.
     */
Gaston Dombiak's avatar
Gaston Dombiak committed
535
    private static class Cache extends LinkedHashMap<String,Object> {
536 537 538 539 540 541 542 543 544 545 546 547

        private int maxSize;

        public Cache(int maxSize) {
            super(64, .75f, true);
            this.maxSize = maxSize;
        }

        protected boolean removeEldestEntry(Map.Entry eldest) {
            return size() > maxSize;
        }
    }
Gaston Dombiak's avatar
Gaston Dombiak committed
548 549 550 551 552 553 554 555 556 557 558 559 560 561 562

    public void writeExternal(ObjectOutput out) throws IOException {
        ExternalizableUtil.getInstance().writeSafeUTF(out, toString());
    }

    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        String jid = ExternalizableUtil.getInstance().readSafeUTF(in);
        String[] parts = getParts(jid);

        this.node = parts[0];
        this.domain = parts[1];
        this.resource = parts[2];
        // Cache the bare and full JID String representation
        updateCache();
    }
563
}