001/** 002 * 003 * Copyright © 2014-2016 Florian Schmaus 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.jxmpp.util; 018 019import org.jxmpp.util.cache.LruCache; 020 021/** 022 * Utility class for handling Strings in XMPP. 023 */ 024public class XmppStringUtils { 025 026 /** 027 * Returns the localpart of an XMPP address (JID). For example, for the address "user@xmpp.org/Resource", "user" 028 * would be returned. If <code>jid</code> is <code>null</code>, then this method returns also <code>null</code>. If 029 * the input String is no valid JID or has no localpart, then this method will return the empty String. 030 * 031 * @param jid 032 * the XMPP address to parse. 033 * @return the name portion of the XMPP address, the empty String or <code>null</code>. 034 */ 035 public static String parseLocalpart(String jid) { 036 if (jid == null) return null; 037 038 int atIndex = jid.indexOf('@'); 039 if (atIndex <= 0) { 040 return ""; 041 } 042 int slashIndex = jid.indexOf('/'); 043 if (slashIndex >= 0 && slashIndex < atIndex) { 044 return ""; 045 } else { 046 return jid.substring(0, atIndex); 047 } 048 } 049 050 /** 051 * Returns the domain of an XMPP address (JID). For example, for the address "user@xmpp.org/Resource", "xmpp.org" 052 * would be returned. If <code>jid</code> is <code>null</code>, then this method returns also <code>null</code>. If 053 * the input String is no valid JID or has no domainpart, then this method will return the empty String. 054 * 055 * @param jid 056 * the XMPP address to parse. 057 * @return the domainpart of the XMPP address, the empty String or <code>null</code>. 058 */ 059 public static String parseDomain(String jid) { 060 if (jid == null) return null; 061 062 int atIndex = jid.indexOf('@'); 063 int slashIndex = jid.indexOf('/'); 064 if (slashIndex > 0) { 065 // 'local@domain.foo/resource' and 'local@domain.foo/res@otherres' case 066 if (slashIndex > atIndex) { 067 return jid.substring(atIndex + 1, slashIndex); 068 // 'domain.foo/res@otherres' case 069 } else { 070 return jid.substring(0, slashIndex); 071 } 072 } else { 073 return jid.substring(atIndex + 1); 074 } 075 } 076 077 /** 078 * Returns the resource portion of an XMPP address (JID). For example, for the address "user@xmpp.org/Resource", 079 * "Resource" would be returned. If <code>jid</code> is <code>null</code>, then this method returns also 080 * <code>null</code>. If the input String is no valid JID or has no resourcepart, then this method will return the 081 * empty String. 082 * 083 * @param jid 084 * the XMPP address to parse. 085 * @return the resource portion of the XMPP address. 086 */ 087 public static String parseResource(String jid) { 088 if (jid == null) return null; 089 090 int slashIndex = jid.indexOf('/'); 091 if (slashIndex + 1 > jid.length() || slashIndex < 0) { 092 return ""; 093 } else { 094 return jid.substring(slashIndex + 1); 095 } 096 } 097 098 /** 099 * Returns the JID with any resource information removed. For example, for 100 * the address "matt@jivesoftware.com/Smack", "matt@jivesoftware.com" would 101 * be returned. 102 * 103 * @param jid 104 * the XMPP JID. 105 * @return the bare XMPP JID without resource information. 106 * @deprecated use {@link #parseBareJid(String)} instead 107 */ 108 @Deprecated 109 public static String parseBareAddress(String jid) { 110 return parseBareJid(jid); 111 } 112 113 /** 114 * Returns the JID with any resource information removed. For example, for 115 * the address "matt@jivesoftware.com/Smack", "matt@jivesoftware.com" would 116 * be returned. 117 * 118 * @param jid 119 * the XMPP JID. 120 * @return the bare XMPP JID without resource information. 121 */ 122 public static String parseBareJid(String jid) { 123 int slashIndex = jid.indexOf('/'); 124 if (slashIndex < 0) { 125 return jid; 126 } else if (slashIndex == 0) { 127 return ""; 128 } else { 129 return jid.substring(0, slashIndex); 130 } 131 } 132 133 /** 134 * Returns true if jid is a full JID (i.e. a JID with resource part). 135 * 136 * @param jid the String to check. 137 * @return true if full JID, false otherwise 138 */ 139 public static boolean isFullJID(String jid) { 140 if (parseLocalpart(jid).length() <= 0 || parseDomain(jid).length() <= 0 141 || parseResource(jid).length() <= 0) { 142 return false; 143 } 144 return true; 145 } 146 147 /** 148 * Returns true if <code>jid</code> is a bare JID ("foo@bar.com"). 149 * <p> 150 * This method may return true for Strings that are not valid JIDs (e.g. because of Stringprep violations). Consider 151 * using <code>org.jxmpp.jid.util.JidUtil.validateBareJid(String)</code> from jxmpp-jid instead of this method as it 152 * exceptions provide a meaningful message string why the JID is not a bare JID and will also check for Stringprep 153 * errors. 154 * </p> 155 * 156 * @param jid the String to check. 157 * @return true if bare JID, false otherwise 158 */ 159 public static boolean isBareJid(String jid) { 160 return parseLocalpart(jid).length() > 0 161 && parseDomain(jid).length() > 0 162 && parseResource(jid).length() == 0; 163 } 164 165 private static final LruCache<String, String> LOCALPART_ESACPE_CACHE = new LruCache<String, String>(100); 166 private static final LruCache<String, String> LOCALPART_UNESCAPE_CACHE = new LruCache<String, String>(100); 167 168 /** 169 * Escapes the localpart of a JID according to "JID Escaping" (XEP-0106). 170 * Escaping replaces characters prohibited by Nodeprep with escape sequences, 171 * as follows: 172 * <table border="1"> 173 * <caption>Character mappings</caption> 174 * <tr><td><b>Unescaped Character</b></td><td><b>Encoded Sequence</b></td></tr> 175 * <tr><td><space></td><td>\20</td></tr> 176 * <tr><td>"</td><td>\22</td></tr> 177 * <tr><td>&</td><td>\26</td></tr> 178 * <tr><td>'</td><td>\27</td></tr> 179 * <tr><td>/</td><td>\2f</td></tr> 180 * <tr><td>:</td><td>\3a</td></tr> 181 * <tr><td><</td><td>\3c</td></tr> 182 * <tr><td>></td><td>\3e</td></tr> 183 * <tr><td>@</td><td>\40</td></tr> 184 * <tr><td>\</td><td>\5c</td></tr> 185 * </table> 186 * 187 * <p> 188 * This process is useful when the localpart comes from an external source that doesn't 189 * conform to Nodeprep. For example, a username in LDAP may be "Joe Smith". Because 190 * the <space> character isn't a valid part of a localpart, the username should 191 * be escaped to "Joe\20Smith" before being made into a JID (e.g. "joe\20smith@example.com" 192 * after case-folding, etc. has been applied). 193 * </p> 194 * 195 * All localpart escaping and un-escaping must be performed manually at the appropriate 196 * time; the JID class will not escape or un-escape automatically. 197 * 198 * @param localpart the localpart. 199 * @return the escaped version of the localpart. 200 * @see <a href="http://xmpp.org/extensions/xep-0106.html">XEP-106: JID Escaping</a> 201 */ 202 public static String escapeLocalpart(String localpart) { 203 if (localpart == null) { 204 return null; 205 } 206 String res = LOCALPART_ESACPE_CACHE.lookup(localpart); 207 if (res != null) { 208 return res; 209 } 210 StringBuilder buf = new StringBuilder(localpart.length() + 8); 211 for (int i = 0, n = localpart.length(); i < n; i++) { 212 char c = localpart.charAt(i); 213 switch (c) { 214 case '"': 215 buf.append("\\22"); 216 break; 217 case '&': 218 buf.append("\\26"); 219 break; 220 case '\'': 221 buf.append("\\27"); 222 break; 223 case '/': 224 buf.append("\\2f"); 225 break; 226 case ':': 227 buf.append("\\3a"); 228 break; 229 case '<': 230 buf.append("\\3c"); 231 break; 232 case '>': 233 buf.append("\\3e"); 234 break; 235 case '@': 236 buf.append("\\40"); 237 break; 238 case '\\': 239 buf.append("\\5c"); 240 break; 241 default: { 242 if (Character.isWhitespace(c)) { 243 buf.append("\\20"); 244 } else { 245 buf.append(c); 246 } 247 } 248 } 249 } 250 res = buf.toString(); 251 LOCALPART_ESACPE_CACHE.put(localpart, res); 252 return res; 253 } 254 255 /** 256 * Un-escapes the localpart of a JID according to "JID Escaping" (XEP-0106). 257 * Escaping replaces characters prohibited by Nodeprep with escape sequences, 258 * as follows: 259 * 260 * <table border="1"> 261 * <caption>Character mapping</caption> 262 * <tr><td><b>Unescaped Character</b></td><td><b>Encoded Sequence</b></td></tr> 263 * <tr><td><space></td><td>\20</td></tr> 264 * <tr><td>"</td><td>\22</td></tr> 265 * <tr><td>&</td><td>\26</td></tr> 266 * <tr><td>'</td><td>\27</td></tr> 267 * <tr><td>/</td><td>\2f</td></tr> 268 * <tr><td>:</td><td>\3a</td></tr> 269 * <tr><td><</td><td>\3c</td></tr> 270 * <tr><td>></td><td>\3e</td></tr> 271 * <tr><td>@</td><td>\40</td></tr> 272 * <tr><td>\</td><td>\5c</td></tr> 273 * </table> 274 * 275 * <p> 276 * This process is useful when the localpart comes from an external source that doesn't 277 * conform to Nodeprep. For example, a username in LDAP may be "Joe Smith". Because 278 * the <space> character isn't a valid part of a localpart, the username should 279 * be escaped to "Joe\20Smith" before being made into a JID (e.g. "joe\20smith@example.com" 280 * after case-folding, etc. has been applied). 281 * </p> 282 * 283 * All localpart escaping and un-escaping must be performed manually at the appropriate 284 * time; the JID class will not escape or un-escape automatically. 285 * 286 * @param localpart the escaped version of the localpart. 287 * @return the un-escaped version of the localpart. 288 * @see <a href="http://xmpp.org/extensions/xep-0106.html">XEP-106: JID Escaping</a> 289 */ 290 public static String unescapeLocalpart(String localpart) { 291 if (localpart == null) { 292 return null; 293 } 294 String res = LOCALPART_UNESCAPE_CACHE.lookup(localpart); 295 if (res != null) { 296 return res; 297 } 298 char[] localpartChars = localpart.toCharArray(); 299 StringBuilder buf = new StringBuilder(localpartChars.length); 300 for (int i = 0, n = localpartChars.length; i < n; i++) { 301 compare: { 302 char c = localpart.charAt(i); 303 if (c == '\\' && i + 2 < n) { 304 char c2 = localpartChars[i + 1]; 305 char c3 = localpartChars[i + 2]; 306 switch(c2) { 307 case '2': 308 switch (c3) { 309 case '0': 310 buf.append(' '); 311 i += 2; 312 break compare; 313 case '2': 314 buf.append('"'); 315 i += 2; 316 break compare; 317 case '6': 318 buf.append('&'); 319 i += 2; 320 break compare; 321 case '7': 322 buf.append('\''); 323 i += 2; 324 break compare; 325 case 'f': 326 buf.append('/'); 327 i += 2; 328 break compare; 329 } 330 break; 331 case '3': 332 switch (c3) { 333 case 'a': 334 buf.append(':'); 335 i += 2; 336 break compare; 337 case 'c': 338 buf.append('<'); 339 i += 2; 340 break compare; 341 case 'e': 342 buf.append('>'); 343 i += 2; 344 break compare; 345 } 346 break; 347 case '4': 348 if (c3 == '0') { 349 buf.append("@"); 350 i += 2; 351 break compare; 352 } 353 break; 354 case '5': 355 if (c3 == 'c') { 356 buf.append("\\"); 357 i += 2; 358 break compare; 359 } 360 break; 361 } 362 } 363 buf.append(c); 364 } 365 } 366 res = buf.toString(); 367 LOCALPART_UNESCAPE_CACHE.put(localpart, res); 368 return res; 369 } 370 371 /** 372 * Construct a JID String from the given parts. 373 * 374 * @param localpart the localpart. 375 * @param domainpart the domainpart. 376 * @return the constructed JID String. 377 */ 378 public static String completeJidFrom(CharSequence localpart, CharSequence domainpart) { 379 return completeJidFrom(localpart != null ? localpart.toString() : null, domainpart.toString()); 380 } 381 382 /** 383 * Construct a JID String from the given parts. 384 * 385 * @param localpart the localpart. 386 * @param domainpart the domainpart. 387 * @return the constructed JID String. 388 */ 389 public static String completeJidFrom(String localpart, String domainpart) { 390 return completeJidFrom(localpart, domainpart, null); 391 } 392 393 /** 394 * Construct a JID String from the given parts. 395 * 396 * @param localpart the localpart. 397 * @param domainpart the domainpart. 398 * @param resource the resourcepart. 399 * @return the constructed JID String. 400 */ 401 public static String completeJidFrom(CharSequence localpart, CharSequence domainpart, CharSequence resource) { 402 return completeJidFrom(localpart != null ? localpart.toString() : null, domainpart.toString(), 403 resource != null ? resource.toString() : null); 404 } 405 406 /** 407 * Construct a JID String from the given parts. 408 * 409 * @param localpart the localpart. 410 * @param domainpart the domainpart. 411 * @param resource the resourcepart. 412 * @return the constructed JID String. 413 */ 414 public static String completeJidFrom(String localpart, String domainpart, String resource) { 415 if (domainpart == null) { 416 throw new IllegalArgumentException("domainpart must not be null"); 417 } 418 int localpartLength = localpart != null ? localpart.length() : 0; 419 int domainpartLength = domainpart.length(); 420 int resourceLength = resource != null ? resource.length() : 0; 421 int maxResLength = localpartLength + domainpartLength + resourceLength + 2; 422 StringBuilder sb = new StringBuilder(maxResLength); 423 if (localpartLength > 0) { 424 sb.append(localpart).append('@'); 425 } 426 sb.append(domainpart); 427 if (resourceLength > 0) { 428 sb.append('/').append(resource); 429 } 430 return sb.toString(); 431 } 432 433 /** 434 * Generate a unique key from a element name and namespace. This key can be used to lookup element/namespace 435 * information. The key is simply generated by concatenating the strings as follows: 436 * <code>element + '\t' + namespace</code>. 437 * <p> 438 * The tab character (\t) was chosen because it will be normalized, i.e. replace by space, in attribute values. It 439 * therefore should never appear in <code>element</code> or <code>namespace</code>. For more information about the 440 * normalization, see the XML specification § <a href="http://www.w3.org/TR/REC-xml/#AVNormalize">3.3.3 441 * Attribute-Value Normalization</a>. 442 * </p> 443 * 444 * @param element the element. 445 * @param namespace the namespace. 446 * @return the unique key of element and namespace. 447 */ 448 public static String generateKey(String element, String namespace) { 449 return element + '\t' + namespace; 450 } 451}