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>&lt;space&gt;</td><td>\20</td></tr>
176         * <tr><td>"</td><td>\22</td></tr>
177         * <tr><td>&amp;</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>&lt;</td><td>\3c</td></tr>
182         * <tr><td>&gt;</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 &lt;space&gt; 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>&lt;space&gt;</td><td>\20</td></tr>
264         * <tr><td>"</td><td>\22</td></tr>
265         * <tr><td>&amp;</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>&lt;</td><td>\3c</td></tr>
270         * <tr><td>&gt;</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 &lt;space&gt; 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}