001: /**
002: * Copyright (C) 2004-2007 Jive Software. All rights reserved.
003: *
004: * This software is published under the terms of the GNU Public License (GPL),
005: * a copy of which is included in this distribution.
006: */package org.xmpp.packet;
007:
008: import org.jivesoftware.stringprep.IDNA;
009: import org.jivesoftware.stringprep.Stringprep;
010: import org.jivesoftware.stringprep.StringprepException;
011: import org.jivesoftware.util.cache.ExternalizableUtil;
012:
013: import java.io.Externalizable;
014: import java.io.IOException;
015: import java.io.ObjectInput;
016: import java.io.ObjectOutput;
017: import java.util.Collections;
018: import java.util.LinkedHashMap;
019: import java.util.Map;
020:
021: /**
022: * An XMPP address (JID). A JID is made up of a node (generally a username), a domain,
023: * and a resource. The node and resource are optional; domain is required. In simple
024: * ABNF form:
025: *
026: * <ul><tt>jid = [ node "@" ] domain [ "/" resource ]</tt></ul>
027: *
028: * Some sample JID's:
029: * <ul>
030: * <li><tt>user@example.com</tt></li>
031: * <li><tt>user@example.com/home</tt></li>
032: * <li><tt>example.com</tt></li>
033: * </ul>
034: *
035: * Each allowable portion of a JID (node, domain, and resource) must not be more
036: * than 1023 bytes in length, resulting in a maximum total size (including the '@'
037: * and '/' separators) of 3071 bytes.
038: *
039: * @author Matt Tucker
040: */
041: public class JID implements Comparable<JID>, Externalizable {
042:
043: // Stringprep operations are very expensive. Therefore, we cache node, domain and
044: // resource values that have already had stringprep applied so that we can check
045: // incoming values against the cache.
046: private static Map<String, Object> stringprepCache = Collections
047: .synchronizedMap(new Cache(10000));
048:
049: private String node;
050: private String domain;
051: private String resource;
052:
053: private String cachedFullJID;
054: private String cachedBareJID;
055:
056: /**
057: * Escapes the node portion of a JID according to "JID Escaping" (JEP-0106).
058: * Escaping replaces characters prohibited by node-prep with escape sequences,
059: * as follows:<p>
060: *
061: * <table border="1">
062: * <tr><td><b>Unescaped Character</b></td><td><b>Encoded Sequence</b></td></tr>
063: * <tr><td><space></td><td>\20</td></tr>
064: * <tr><td>"</td><td>\22</td></tr>
065: * <tr><td>&</td><td>\26</td></tr>
066: * <tr><td>'</td><td>\27</td></tr>
067: * <tr><td>/</td><td>\2f</td></tr>
068: * <tr><td>:</td><td>\3a</td></tr>
069: * <tr><td><</td><td>\3c</td></tr>
070: * <tr><td>></td><td>\3e</td></tr>
071: * <tr><td>@</td><td>\40</td></tr>
072: * <tr><td>\</td><td>\5c</td></tr>
073: * </table><p>
074: *
075: * This process is useful when the node comes from an external source that doesn't
076: * conform to nodeprep. For example, a username in LDAP may be "Joe Smith". Because
077: * the <space> character isn't a valid part of a node, the username should
078: * be escaped to "Joe\20Smith" before being made into a JID (e.g. "joe\20smith@example.com"
079: * after case-folding, etc. has been applied).<p>
080: *
081: * All node escaping and un-escaping must be performed manually at the appropriate
082: * time; the JID class will not escape or un-escape automatically.
083: *
084: * @param node the node.
085: * @return the escaped version of the node.
086: */
087: public static String escapeNode(String node) {
088: if (node == null) {
089: return null;
090: }
091: StringBuilder buf = new StringBuilder(node.length() + 8);
092: for (int i = 0, n = node.length(); i < n; i++) {
093: char c = node.charAt(i);
094: switch (c) {
095: case '"':
096: buf.append("\\22");
097: break;
098: case '&':
099: buf.append("\\26");
100: break;
101: case '\'':
102: buf.append("\\27");
103: break;
104: case '/':
105: buf.append("\\2f");
106: break;
107: case ':':
108: buf.append("\\3a");
109: break;
110: case '<':
111: buf.append("\\3c");
112: break;
113: case '>':
114: buf.append("\\3e");
115: break;
116: case '@':
117: buf.append("\\40");
118: break;
119: case '\\':
120: buf.append("\\5c");
121: break;
122: default: {
123: if (Character.isWhitespace(c)) {
124: buf.append("\\20");
125: } else {
126: buf.append(c);
127: }
128: }
129: }
130: }
131: return buf.toString();
132: }
133:
134: /**
135: * Un-escapes the node portion of a JID according to "JID Escaping" (JEP-0106).<p>
136: * Escaping replaces characters prohibited by node-prep with escape sequences,
137: * as follows:<p>
138: *
139: * <table border="1">
140: * <tr><td><b>Unescaped Character</b></td><td><b>Encoded Sequence</b></td></tr>
141: * <tr><td><space></td><td>\20</td></tr>
142: * <tr><td>"</td><td>\22</td></tr>
143: * <tr><td>&</td><td>\26</td></tr>
144: * <tr><td>'</td><td>\27</td></tr>
145: * <tr><td>/</td><td>\2f</td></tr>
146: * <tr><td>:</td><td>\3a</td></tr>
147: * <tr><td><</td><td>\3c</td></tr>
148: * <tr><td>></td><td>\3e</td></tr>
149: * <tr><td>@</td><td>\40</td></tr>
150: * <tr><td>\</td><td>\5c</td></tr>
151: * </table><p>
152: *
153: * This process is useful when the node comes from an external source that doesn't
154: * conform to nodeprep. For example, a username in LDAP may be "Joe Smith". Because
155: * the <space> character isn't a valid part of a node, the username should
156: * be escaped to "Joe\20Smith" before being made into a JID (e.g. "joe\20smith@example.com"
157: * after case-folding, etc. has been applied).<p>
158: *
159: * All node escaping and un-escaping must be performed manually at the appropriate
160: * time; the JID class will not escape or un-escape automatically.
161: *
162: * @param node the escaped version of the node.
163: * @return the un-escaped version of the node.
164: */
165: public static String unescapeNode(String node) {
166: if (node == null) {
167: return null;
168: }
169: char[] nodeChars = node.toCharArray();
170: StringBuilder buf = new StringBuilder(nodeChars.length);
171: for (int i = 0, n = nodeChars.length; i < n; i++) {
172: compare: {
173: char c = node.charAt(i);
174: if (c == '\\' && i + 2 < n) {
175: char c2 = nodeChars[i + 1];
176: char c3 = nodeChars[i + 2];
177: if (c2 == '2') {
178: switch (c3) {
179: case '0':
180: buf.append(' ');
181: i += 2;
182: break compare;
183: case '2':
184: buf.append('"');
185: i += 2;
186: break compare;
187: case '6':
188: buf.append('&');
189: i += 2;
190: break compare;
191: case '7':
192: buf.append('\'');
193: i += 2;
194: break compare;
195: case 'f':
196: buf.append('/');
197: i += 2;
198: break compare;
199: }
200: } else if (c2 == '3') {
201: switch (c3) {
202: case 'a':
203: buf.append(':');
204: i += 2;
205: break compare;
206: case 'c':
207: buf.append('<');
208: i += 2;
209: break compare;
210: case 'e':
211: buf.append('>');
212: i += 2;
213: break compare;
214: }
215: } else if (c2 == '4') {
216: if (c3 == '0') {
217: buf.append("@");
218: i += 2;
219: break compare;
220: }
221: } else if (c2 == '5') {
222: if (c3 == 'c') {
223: buf.append("\\");
224: i += 2;
225: break compare;
226: }
227: }
228: }
229: buf.append(c);
230: }
231: }
232: return buf.toString();
233: }
234:
235: public static String resourceprep(String resource)
236: throws StringprepException {
237: String answer = resource;
238: if (!stringprepCache.containsKey(resource)) {
239: answer = Stringprep.resourceprep(resource);
240: // Validate field is not greater than 1023 bytes. UTF-8 characters use two bytes.
241: if (answer != null && answer.length() * 2 > 1023) {
242: return answer;
243: }
244: stringprepCache.put(answer, null);
245: }
246: return answer;
247: }
248:
249: /**
250: * Constructor added for Externalizable. Do not use this constructor.
251: */
252: public JID() {
253: }
254:
255: /**
256: * Constructs a JID from it's String representation.
257: *
258: * @param jid a valid JID.
259: * @throws IllegalArgumentException if the JID is not valid.
260: */
261: public JID(String jid) {
262: if (jid == null) {
263: throw new NullPointerException("JID cannot be null");
264: }
265: String[] parts = getParts(jid);
266:
267: init(parts[0], parts[1], parts[2]);
268: }
269:
270: /**
271: * Constructs a JID given a node, domain, and resource.
272: *
273: * @param node the node.
274: * @param domain the domain, which must not be <tt>null</tt>.
275: * @param resource the resource.
276: * @throws IllegalArgumentException if the JID is not valid.
277: */
278: public JID(String node, String domain, String resource) {
279: if (domain == null) {
280: throw new NullPointerException("Domain cannot be null");
281: }
282: init(node, domain, resource);
283: }
284:
285: /**
286: * Constructs a JID given a node, domain, and resource being able to specify if stringprep
287: * should be applied or not.
288: *
289: * @param node the node.
290: * @param domain the domain, which must not be <tt>null</tt>.
291: * @param resource the resource.
292: * @param skipStringprep true if stringprep should not be applied.
293: * @throws IllegalArgumentException if the JID is not valid.
294: */
295: public JID(String node, String domain, String resource,
296: boolean skipStringprep) {
297: if (domain == null) {
298: throw new NullPointerException("Domain cannot be null");
299: }
300: if (skipStringprep) {
301: this .node = node;
302: this .domain = domain;
303: this .resource = resource;
304: // Cache the bare and full JID String representation
305: updateCache();
306: } else {
307: init(node, domain, resource);
308: }
309: }
310:
311: /**
312: * Returns a String array with the parsed node, domain and resource.
313: * No Stringprep is performed while parsing the textual representation.
314: *
315: * @param jid the textual JID representation.
316: * @return a string array with the parsed node, domain and resource.
317: */
318: static String[] getParts(String jid) {
319: String[] parts = new String[3];
320: String node = null, domain, resource;
321: if (jid == null) {
322: return parts;
323: }
324:
325: int atIndex = jid.indexOf("@");
326: int slashIndex = jid.indexOf("/");
327:
328: // Node
329: if (atIndex > 0) {
330: node = jid.substring(0, atIndex);
331: }
332:
333: // Domain
334: if (atIndex + 1 > jid.length()) {
335: throw new IllegalArgumentException(
336: "JID with empty domain not valid");
337: }
338: if (atIndex < 0) {
339: if (slashIndex > 0) {
340: domain = jid.substring(0, slashIndex);
341: } else {
342: domain = jid;
343: }
344: } else {
345: if (slashIndex > 0) {
346: domain = jid.substring(atIndex + 1, slashIndex);
347: } else {
348: domain = jid.substring(atIndex + 1);
349: }
350: }
351:
352: // Resource
353: if (slashIndex + 1 > jid.length() || slashIndex < 0) {
354: resource = null;
355: } else {
356: resource = jid.substring(slashIndex + 1);
357: }
358: parts[0] = node;
359: parts[1] = domain;
360: parts[2] = resource;
361: return parts;
362: }
363:
364: /**
365: * Transforms the JID parts using the appropriate Stringprep profiles, then
366: * validates them. If they are fully valid, the field values are saved, otherwise
367: * an IllegalArgumentException is thrown.
368: *
369: * @param node the node.
370: * @param domain the domain.
371: * @param resource the resource.
372: */
373: private void init(String node, String domain, String resource) {
374: // Set node and resource to null if they are the empty string.
375: if (node != null && node.equals("")) {
376: node = null;
377: }
378: if (resource != null && resource.equals("")) {
379: resource = null;
380: }
381: // Stringprep (node prep, resourceprep, etc).
382: try {
383: if (!stringprepCache.containsKey(node)) {
384: this .node = Stringprep.nodeprep(node);
385: // Validate field is not greater than 1023 bytes. UTF-8 characters use two bytes.
386: if (this .node != null && this .node.length() * 2 > 1023) {
387: throw new IllegalArgumentException(
388: "Node cannot be larger than 1023 bytes. "
389: + "Size is "
390: + (this .node.length() * 2)
391: + " bytes.");
392: }
393: stringprepCache.put(this .node, null);
394: } else {
395: this .node = node;
396: }
397: // XMPP specifies that domains should be run through IDNA and
398: // that they should be run through nameprep before doing any
399: // comparisons. We always run the domain through nameprep to
400: // make comparisons easier later.
401: if (!stringprepCache.containsKey(domain)) {
402: this .domain = Stringprep.nameprep(IDNA.toASCII(domain),
403: false);
404: // Validate field is not greater than 1023 bytes. UTF-8 characters use two bytes.
405: if (this .domain.length() * 2 > 1023) {
406: throw new IllegalArgumentException(
407: "Domain cannot be larger than 1023 bytes. "
408: + "Size is "
409: + (this .domain.length() * 2)
410: + " bytes.");
411: }
412: stringprepCache.put(this .domain, null);
413: } else {
414: this .domain = domain;
415: }
416: this .resource = resourceprep(resource);
417: // Validate field is not greater than 1023 bytes. UTF-8 characters use two bytes.
418: if (resource != null && resource.length() * 2 > 1023) {
419: throw new IllegalArgumentException(
420: "Resource cannot be larger than 1023 bytes. "
421: + "Size is " + (resource.length() * 2)
422: + " bytes.");
423: }
424: // Cache the bare and full JID String representation
425: updateCache();
426: } catch (Exception e) {
427: StringBuilder buf = new StringBuilder();
428: if (node != null) {
429: buf.append(node).append("@");
430: }
431: buf.append(domain);
432: if (resource != null) {
433: buf.append("/").append(resource);
434: }
435: throw new IllegalArgumentException("Illegal JID: "
436: + buf.toString(), e);
437: }
438: }
439:
440: private void updateCache() {
441: // Cache the bare JID
442: StringBuilder buf = new StringBuilder(40);
443: if (node != null) {
444: buf.append(node).append("@");
445: }
446: buf.append(domain);
447: cachedBareJID = buf.toString();
448:
449: // Cache the full JID
450: if (resource != null) {
451: buf.append("/").append(resource);
452: cachedFullJID = buf.toString();
453: } else {
454: cachedFullJID = cachedBareJID;
455: }
456: }
457:
458: /**
459: * Returns the node, or <tt>null</tt> if this JID does not contain node information.
460: *
461: * @return the node.
462: */
463: public String getNode() {
464: return node;
465: }
466:
467: /**
468: * Returns the domain.
469: *
470: * @return the domain.
471: */
472: public String getDomain() {
473: return domain;
474: }
475:
476: /**
477: * Returns the resource, or <tt>null</tt> if this JID does not contain resource information.
478: *
479: * @return the resource.
480: */
481: public String getResource() {
482: return resource;
483: }
484:
485: /**
486: * Returns the String representation of the bare JID, which is the JID with
487: * resource information removed.
488: *
489: * @return the bare JID.
490: */
491: public String toBareJID() {
492: return cachedBareJID;
493: }
494:
495: /**
496: * Returns a String representation of the JID.
497: *
498: * @return a String representation of the JID.
499: */
500: public String toString() {
501: return cachedFullJID;
502: }
503:
504: public int hashCode() {
505: return toString().hashCode();
506: }
507:
508: public boolean equals(Object object) {
509: if (!(object instanceof JID)) {
510: return false;
511: }
512: if (this == object) {
513: return true;
514: }
515: JID jid = (JID) object;
516: // Node. If node isn't null, compare.
517: if (node != null) {
518: if (!node.equals(jid.node)) {
519: return false;
520: }
521: }
522: // Otherwise, jid.node must be null.
523: else if (jid.node != null) {
524: return false;
525: }
526: // Compare domain, which must be null.
527: if (!domain.equals(jid.domain)) {
528: return false;
529: }
530: // Resource. If resource isn't null, compare.
531: if (resource != null) {
532: if (!resource.equals(jid.resource)) {
533: return false;
534: }
535: }
536: // Otherwise, jid.resource must be null.
537: else if (jid.resource != null) {
538: return false;
539: }
540: // Passed all checks, so equal.
541: return true;
542: }
543:
544: public int compareTo(JID jid) {
545: // Comparison order is domain, node, resource.
546: int compare = domain.compareTo(jid.domain);
547: if (compare == 0) {
548: String myNode = node != null ? node : "";
549: String hisNode = jid.node != null ? jid.node : "";
550: compare = myNode.compareTo(hisNode);
551: }
552: if (compare == 0) {
553: String myResource = resource != null ? resource : "";
554: String hisResource = jid.resource != null ? jid.resource
555: : "";
556: compare = myResource.compareTo(hisResource);
557: }
558: return compare;
559: }
560:
561: /**
562: * Returns true if two JID's are equivalent. The JID components are compared using
563: * the following rules:<ul>
564: * <li>Nodes are normalized using nodeprep (case insensitive).
565: * <li>Domains are normalized using IDNA and then nameprep (case insensitive).
566: * <li>Resources are normalized using resourceprep (case sensitive).</ul>
567: *
568: * These normalization rules ensure, for example, that
569: * <tt>User@EXAMPLE.com/home</tt> is considered equal to <tt>user@example.com/home</tt>.
570: *
571: * @param jid1 a JID.
572: * @param jid2 a JID.
573: * @return true if the JIDs are equivalent; false otherwise.
574: * @throws IllegalArgumentException if either JID is not valid.
575: */
576: public static boolean equals(String jid1, String jid2) {
577: return new JID(jid1).equals(new JID(jid2));
578: }
579:
580: /**
581: * A simple cache class that extends LinkedHashMap. It uses an LRU policy to
582: * keep the cache at a maximum size.
583: */
584: private static class Cache extends LinkedHashMap<String, Object> {
585:
586: private int maxSize;
587:
588: public Cache(int maxSize) {
589: super (64, .75f, true);
590: this .maxSize = maxSize;
591: }
592:
593: protected boolean removeEldestEntry(Map.Entry eldest) {
594: return size() > maxSize;
595: }
596: }
597:
598: public void writeExternal(ObjectOutput out) throws IOException {
599: ExternalizableUtil.getInstance().writeSafeUTF(out, toString());
600: }
601:
602: public void readExternal(ObjectInput in) throws IOException,
603: ClassNotFoundException {
604: String jid = ExternalizableUtil.getInstance().readSafeUTF(in);
605: String[] parts = getParts(jid);
606:
607: this .node = parts[0];
608: this .domain = parts[1];
609: this .resource = parts[2];
610: // Cache the bare and full JID String representation
611: updateCache();
612: }
613: }
|