1 /*
2 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3 *
4 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
5 *
6 * The contents of this file are subject to the terms of either the GNU
7 * General Public License Version 2 only ("GPL") or the Common Development
8 * and Distribution License("CDDL") (collectively, the "License"). You
9 * may not use this file except in compliance with the License. You can obtain
10 * a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
11 * or glassfish/bootstrap/legal/LICENSE.txt. See the License for the specific
12 * language governing permissions and limitations under the License.
13 *
14 * When distributing the software, include this License Header Notice in each
15 * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
16 * Sun designates this particular file as subject to the "Classpath" exception
17 * as provided by Sun in the GPL Version 2 section of the License file that
18 * accompanied this code. If applicable, add the following below the License
19 * Header, with the fields enclosed by brackets [] replaced by your own
20 * identifying information: "Portions Copyrighted [year]
21 * [name of copyright owner]"
22 *
23 * Contributor(s):
24 *
25 * If you wish your version of this file to be governed by only the CDDL or
26 * only the GPL Version 2, indicate your decision by adding "[Contributor]
27 * elects to include this software in this distribution under the [CDDL or GPL
28 * Version 2] license." If you don't indicate a single choice of license, a
29 * recipient has the option to distribute your version of this file under
30 * either the CDDL, the GPL Version 2 or to extend the choice of license to
31 * its licensees as provided above. However, if you add GPL Version 2 code
32 * and therefore, elected the GPL Version 2 license, then the option applies
33 * only if the new code is made subject to such option by the copyright
34 * holder.
35 */
36
37 /*
38 * @(#)URLName.java 1.19 07/05/04
39 */
40
41 package javax.mail;
42
43 import java.net;
44
45 import java.io.ByteArrayOutputStream;
46 import java.io.OutputStreamWriter;
47 import java.io.IOException;
48 import java.io.UnsupportedEncodingException;
49 import java.util.BitSet;
50 import java.util.Locale;
51
52
53 /**
54 * The name of a URL. This class represents a URL name and also
55 * provides the basic parsing functionality to parse most internet
56 * standard URL schemes. <p>
57 *
58 * Note that this class differs from <code>java.net.URL</code>
59 * in that this class just represents the name of a URL, it does
60 * not model the connection to a URL.
61 *
62 * @version 1.19, 07/05/04
63 * @author Christopher Cotton
64 * @author Bill Shannon
65 */
66
67 public class URLName {
68
69 /**
70 * The full version of the URL
71 */
72 protected String fullURL;
73
74 /**
75 * The protocol to use (ftp, http, nntp, imap, pop3 ... etc.) .
76 */
77 private String protocol;
78
79 /**
80 * The username to use when connecting
81 */
82 private String username;
83
84 /**
85 * The password to use when connecting.
86 */
87 private String password;
88
89 /**
90 * The host name to which to connect.
91 */
92 private String host;
93
94 /**
95 * The host's IP address, used in equals and hashCode.
96 * Computed on demand.
97 */
98 private InetAddress hostAddress;
99 private boolean hostAddressKnown = false;
100
101 /**
102 * The protocol port to connect to.
103 */
104 private int port = -1;
105
106 /**
107 * The specified file name on that host.
108 */
109 private String file;
110
111 /**
112 * # reference.
113 */
114 private String ref;
115
116 /**
117 * Our hash code.
118 */
119 private int hashCode = 0;
120
121 /**
122 * A way to turn off encoding, just in case...
123 */
124 private static boolean doEncode = true;
125
126 static {
127 try {
128 doEncode = !Boolean.getBoolean("mail.URLName.dontencode");
129 } catch (Exception ex) {
130 // ignore any errors
131 }
132 }
133
134 /**
135 * Creates a URLName object from the specified protocol,
136 * host, port number, file, username, and password. Specifying a port
137 * number of -1 indicates that the URL should use the default port for
138 * the protocol.
139 */
140 public URLName(
141 String protocol,
142 String host,
143 int port,
144 String file,
145 String username,
146 String password
147 )
148 {
149 this.protocol = protocol;
150 this.host = host;
151 this.port = port;
152 int refStart;
153 if (file != null && (refStart = file.indexOf('#')) != -1) {
154 this.file = file.substring(0, refStart);
155 this.ref = file.substring(refStart + 1);
156 } else {
157 this.file = file;
158 this.ref = null;
159 }
160 this.username = doEncode ? encode(username) : username;
161 this.password = doEncode ? encode(password) : password;
162 }
163
164 /**
165 * Construct a URLName from a java.net.URL object.
166 */
167 public URLName(URL url) {
168 this(url.toString());
169 }
170
171 /**
172 * Construct a URLName from the string. Parses out all the possible
173 * information (protocol, host, port, file, username, password).
174 */
175 public URLName(String url) {
176 parseString(url);
177 }
178
179 /**
180 * Constructs a string representation of this URLName.
181 */
182 public String toString() {
183 if (fullURL == null) {
184 // add the "protocol:"
185 StringBuffer tempURL = new StringBuffer();
186 if (protocol != null) {
187 tempURL.append(protocol);
188 tempURL.append(":");
189 }
190
191 if (username != null || host != null) {
192 // add the "//"
193 tempURL.append("//");
194
195 // add the user:password@
196 // XXX - can you just have a password? without a username?
197 if (username != null) {
198 tempURL.append(username);
199
200 if (password != null){
201 tempURL.append(":");
202 tempURL.append(password);
203 }
204
205 tempURL.append("@");
206 }
207
208 // add host
209 if (host != null) {
210 tempURL.append(host);
211 }
212
213 // add port (if needed)
214 if (port != -1) {
215 tempURL.append(":");
216 tempURL.append(Integer.toString(port));
217 }
218 if (file != null)
219 tempURL.append("/");
220 }
221
222 // add the file
223 if (file != null) {
224 tempURL.append(file);
225 }
226
227 // add the ref
228 if (ref != null) {
229 tempURL.append("#");
230 tempURL.append(ref);
231 }
232
233 // create the fullURL now
234 fullURL = tempURL.toString();
235 }
236
237 return fullURL;
238 }
239
240 /**
241 * Method which does all of the work of parsing the string.
242 */
243 protected void parseString(String url) {
244 // initialize everything in case called from subclass
245 // (URLName really should be a final class)
246 protocol = file = ref = host = username = password = null;
247 port = -1;
248
249 int len = url.length();
250
251 // find the protocol
252 // XXX - should check for only legal characters before the colon
253 // (legal: a-z, A-Z, 0-9, "+", ".", "-")
254 int protocolEnd = url.indexOf(':');
255 if (protocolEnd != -1)
256 protocol = url.substring(0, protocolEnd);
257
258 // is this an Internet standard URL that contains a host name?
259 if (url.regionMatches(protocolEnd + 1, "//", 0, 2)) {
260 // find where the file starts
261 String fullhost = null;
262 int fileStart = url.indexOf('/', protocolEnd + 3);
263 if (fileStart != -1) {
264 fullhost = url.substring(protocolEnd + 3, fileStart);
265 if (fileStart + 1 < len)
266 file = url.substring(fileStart + 1);
267 else
268 file = "";
269 } else
270 fullhost = url.substring(protocolEnd + 3);
271
272 // examine the fullhost, for username password etc.
273 int i = fullhost.indexOf('@');
274 if (i != -1) {
275 String fulluserpass = fullhost.substring(0, i);
276 fullhost = fullhost.substring(i + 1);
277
278 // get user and password
279 int passindex = fulluserpass.indexOf(':');
280 if (passindex != -1) {
281 username = fulluserpass.substring(0, passindex);
282 password = fulluserpass.substring(passindex + 1);
283 } else {
284 username = fulluserpass;
285 }
286 }
287
288 // get the port (if there)
289 int portindex;
290 if (fullhost.length() > 0 && fullhost.charAt(0) == '[') {
291 // an IPv6 address?
292 portindex = fullhost.indexOf(':', fullhost.indexOf(']'));
293 } else {
294 portindex = fullhost.indexOf(':');
295 }
296 if (portindex != -1) {
297 String portstring = fullhost.substring(portindex + 1);
298 if (portstring.length() > 0) {
299 try {
300 port = Integer.parseInt(portstring);
301 } catch (NumberFormatException nfex) {
302 port = -1;
303 }
304 }
305
306 host = fullhost.substring(0, portindex);
307 } else {
308 host = fullhost;
309 }
310 } else {
311 if (protocolEnd + 1 < len)
312 file = url.substring(protocolEnd + 1);
313 }
314
315 // extract the reference from the file name, if any
316 int refStart;
317 if (file != null && (refStart = file.indexOf('#')) != -1) {
318 ref = file.substring(refStart + 1);
319 file = file.substring(0, refStart);
320 }
321 }
322
323 /**
324 * Returns the port number of this URLName.
325 * Returns -1 if the port is not set.
326 */
327 public int getPort() {
328 return port;
329 }
330
331 /**
332 * Returns the protocol of this URLName.
333 * Returns null if this URLName has no protocol.
334 */
335 public String getProtocol() {
336 return protocol;
337 }
338
339 /**
340 * Returns the file name of this URLName.
341 * Returns null if this URLName has no file name.
342 */
343 public String getFile() {
344 return file;
345 }
346
347 /**
348 * Returns the reference of this URLName.
349 * Returns null if this URLName has no reference.
350 */
351 public String getRef() {
352 return ref;
353 }
354
355 /**
356 * Returns the host of this URLName.
357 * Returns null if this URLName has no host.
358 */
359 public String getHost() {
360 return host;
361 }
362
363 /**
364 * Returns the user name of this URLName.
365 * Returns null if this URLName has no user name.
366 */
367 public String getUsername() {
368 return doEncode ? decode(username) : username;
369 }
370
371 /**
372 * Returns the password of this URLName.
373 * Returns null if this URLName has no password.
374 */
375 public String getPassword() {
376 return doEncode ? decode(password) : password;
377 }
378
379 /**
380 * Constructs a URL from the URLName.
381 */
382 public URL getURL() throws MalformedURLException {
383 return new URL(getProtocol(), getHost(), getPort(), getFile());
384 }
385
386 /**
387 * Compares two URLNames. The result is true if and only if the
388 * argument is not null and is a URLName object that represents the
389 * same URLName as this object. Two URLName objects are equal if
390 * they have the same protocol and the same host,
391 * the same port number on the host, the same username,
392 * and the same file on the host. The fields (host, username,
393 * file) are also considered the same if they are both
394 * null. <p>
395 *
396 * Hosts are considered equal if the names are equal (case independent)
397 * or if host name lookups for them both succeed and they both reference
398 * the same IP address. <p>
399 *
400 * Note that URLName has no knowledge of default port numbers for
401 * particular protocols, so "imap://host" and "imap://host:143"
402 * would not compare as equal. <p>
403 *
404 * Note also that the password field is not included in the comparison,
405 * nor is any reference field appended to the filename.
406 */
407 public boolean equals(Object obj) {
408 if (!(obj instanceof URLName))
409 return false;
410 URLName u2 = (URLName)obj;
411
412 // compare protocols
413 if (u2.protocol == null || !u2.protocol.equals(protocol))
414 return false;
415
416 // compare hosts
417 InetAddress a1 = getHostAddress(), a2 = u2.getHostAddress();
418 // if we have internet address for both, and they're not the same, fail
419 if (a1 != null && a2 != null) {
420 if (!a1.equals(a2))
421 return false;
422 // else, if we have host names for both, and they're not the same, fail
423 } else if (host != null && u2.host != null) {
424 if (!host.equalsIgnoreCase(u2.host))
425 return false;
426 // else, if not both null
427 } else if (host != u2.host) {
428 return false;
429 }
430 // at this point, hosts match
431
432 // compare usernames
433 if (!(username == u2.username ||
434 (username != null && username.equals(u2.username))))
435 return false;
436
437 // Forget about password since it doesn't
438 // really denote a different store.
439
440 // compare files
441 String f1 = file == null ? "" : file;
442 String f2 = u2.file == null ? "" : u2.file;
443
444 if (!f1.equals(f2))
445 return false;
446
447 // compare ports
448 if (port != u2.port)
449 return false;
450
451 // all comparisons succeeded, they're equal
452 return true;
453 }
454
455 /**
456 * Compute the hash code for this URLName.
457 */
458 public int hashCode() {
459 if (hashCode != 0)
460 return hashCode;
461 if (protocol != null)
462 hashCode += protocol.hashCode();
463 InetAddress addr = getHostAddress();
464 if (addr != null)
465 hashCode += addr.hashCode();
466 else if (host != null)
467 hashCode += host.toLowerCase(Locale.ENGLISH).hashCode();
468 if (username != null)
469 hashCode += username.hashCode();
470 if (file != null)
471 hashCode += file.hashCode();
472 hashCode += port;
473 return hashCode;
474 }
475
476 /**
477 * Get the IP address of our host. Look up the
478 * name the first time and remember that we've done
479 * so, whether the lookup fails or not.
480 */
481 private synchronized InetAddress getHostAddress() {
482 if (hostAddressKnown)
483 return hostAddress;
484 if (host == null)
485 return null;
486 try {
487 hostAddress = InetAddress.getByName(host);
488 } catch (UnknownHostException ex) {
489 hostAddress = null;
490 }
491 hostAddressKnown = true;
492 return hostAddress;
493 }
494
495 /**
496 * The class contains a utility method for converting a
497 * <code>String</code> into a MIME format called
498 * "<code>x-www-form-urlencoded</code>" format.
499 * <p>
500 * To convert a <code>String</code>, each character is examined in turn:
501 * <ul>
502 * <li>The ASCII characters '<code>a</code>' through '<code>z</code>',
503 * '<code>A</code>' through '<code>Z</code>', '<code>0</code>'
504 * through '<code>9</code>', and ".", "-",
505 * "*", "_" remain the same.
506 * <li>The space character '<code> </code>' is converted into a
507 * plus sign '<code>+</code>'.
508 * <li>All other characters are converted into the 3-character string
509 * "<code>%<i>xy</i></code>", where <i>xy</i> is the two-digit
510 * hexadecimal representation of the lower 8-bits of the character.
511 * </ul>
512 *
513 * @author Herb Jellinek
514 * @version 1.16, 10/23/99
515 * @since JDK1.0
516 */
517 static BitSet dontNeedEncoding;
518 static final int caseDiff = ('a' - 'A');
519
520 /* The list of characters that are not encoded have been determined by
521 referencing O'Reilly's "HTML: The Definitive Guide" (page 164). */
522
523 static {
524 dontNeedEncoding = new BitSet(256);
525 int i;
526 for (i = 'a'; i <= 'z'; i++) {
527 dontNeedEncoding.set(i);
528 }
529 for (i = 'A'; i <= 'Z'; i++) {
530 dontNeedEncoding.set(i);
531 }
532 for (i = '0'; i <= '9'; i++) {
533 dontNeedEncoding.set(i);
534 }
535 /* encoding a space to a + is done in the encode() method */
536 dontNeedEncoding.set(' ');
537 dontNeedEncoding.set('-');
538 dontNeedEncoding.set('_');
539 dontNeedEncoding.set('.');
540 dontNeedEncoding.set('*');
541 }
542
543 /**
544 * Translates a string into <code>x-www-form-urlencoded</code> format.
545 *
546 * @param s <code>String</code> to be translated.
547 * @return the translated <code>String</code>.
548 */
549 static String encode(String s) {
550 if (s == null)
551 return null;
552 // the common case is no encoding is needed
553 for (int i = 0; i < s.length(); i++) {
554 int c = (int)s.charAt(i);
555 if (c == ' ' || !dontNeedEncoding.get(c))
556 return _encode(s);
557 }
558 return s;
559 }
560
561 private static String _encode(String s) {
562 int maxBytesPerChar = 10;
563 StringBuffer out = new StringBuffer(s.length());
564 ByteArrayOutputStream buf = new ByteArrayOutputStream(maxBytesPerChar);
565 OutputStreamWriter writer = new OutputStreamWriter(buf);
566
567 for (int i = 0; i < s.length(); i++) {
568 int c = (int)s.charAt(i);
569 if (dontNeedEncoding.get(c)) {
570 if (c == ' ') {
571 c = '+';
572 }
573 out.append((char)c);
574 } else {
575 // convert to external encoding before hex conversion
576 try {
577 writer.write(c);
578 writer.flush();
579 } catch(IOException e) {
580 buf.reset();
581 continue;
582 }
583 byte[] ba = buf.toByteArray();
584 for (int j = 0; j < ba.length; j++) {
585 out.append('%');
586 char ch = Character.forDigit((ba[j] >> 4) & 0xF, 16);
587 // converting to use uppercase letter as part of
588 // the hex value if ch is a letter.
589 if (Character.isLetter(ch)) {
590 ch -= caseDiff;
591 }
592 out.append(ch);
593 ch = Character.forDigit(ba[j] & 0xF, 16);
594 if (Character.isLetter(ch)) {
595 ch -= caseDiff;
596 }
597 out.append(ch);
598 }
599 buf.reset();
600 }
601 }
602
603 return out.toString();
604 }
605
606
607 /**
608 * The class contains a utility method for converting from
609 * a MIME format called "<code>x-www-form-urlencoded</code>"
610 * to a <code>String</code>
611 * <p>
612 * To convert to a <code>String</code>, each character is examined in turn:
613 * <ul>
614 * <li>The ASCII characters '<code>a</code>' through '<code>z</code>',
615 * '<code>A</code>' through '<code>Z</code>', and '<code>0</code>'
616 * through '<code>9</code>' remain the same.
617 * <li>The plus sign '<code>+</code>'is converted into a
618 * space character '<code> </code>'.
619 * <li>The remaining characters are represented by 3-character
620 * strings which begin with the percent sign,
621 * "<code>%<i>xy</i></code>", where <i>xy</i> is the two-digit
622 * hexadecimal representation of the lower 8-bits of the character.
623 * </ul>
624 *
625 * @author Mark Chamness
626 * @author Michael McCloskey
627 * @version 1.7, 10/22/99
628 * @since 1.2
629 */
630
631 /**
632 * Decodes a "x-www-form-urlencoded"
633 * to a <tt>String</tt>.
634 * @param s the <code>String</code> to decode
635 * @return the newly decoded <code>String</code>
636 */
637 static String decode(String s) {
638 if (s == null)
639 return null;
640 if (indexOfAny(s, "+%") == -1)
641 return s; // the common case
642
643 StringBuffer sb = new StringBuffer();
644 for (int i = 0; i < s.length(); i++) {
645 char c = s.charAt(i);
646 switch (c) {
647 case '+':
648 sb.append(' ');
649 break;
650 case '%':
651 try {
652 sb.append((char)Integer.parseInt(
653 s.substring(i+1,i+3),16));
654 } catch (NumberFormatException e) {
655 throw new IllegalArgumentException();
656 }
657 i += 2;
658 break;
659 default:
660 sb.append(c);
661 break;
662 }
663 }
664 // Undo conversion to external encoding
665 String result = sb.toString();
666 try {
667 byte[] inputBytes = result.getBytes("8859_1");
668 result = new String(inputBytes);
669 } catch (UnsupportedEncodingException e) {
670 // The system should always have 8859_1
671 }
672 return result;
673 }
674
675 /**
676 * Return the first index of any of the characters in "any" in "s",
677 * or -1 if none are found.
678 *
679 * This should be a method on String.
680 */
681 private static int indexOfAny(String s, String any) {
682 return indexOfAny(s, any, 0);
683 }
684
685 private static int indexOfAny(String s, String any, int start) {
686 try {
687 int len = s.length();
688 for (int i = start; i < len; i++) {
689 if (any.indexOf(s.charAt(i)) >= 0)
690 return i;
691 }
692 return -1;
693 } catch (StringIndexOutOfBoundsException e) {
694 return -1;
695 }
696 }
697
698 /*
699 // Do not remove, this is needed when testing new URL cases
700 public static void main(String[] argv) {
701 String [] testURLNames = {
702 "protocol://userid:password@host:119/file",
703 "http://funny/folder/file.html",
704 "http://funny/folder/file.html#ref",
705 "http://funny/folder/file.html#",
706 "http://funny/#ref",
707 "imap://jmr:secret@labyrinth//var/mail/jmr",
708 "nntp://fred@labyrinth:143/save/it/now.mbox",
709 "imap://jmr@labyrinth/INBOX",
710 "imap://labryrinth",
711 "imap://labryrinth/",
712 "file:",
713 "file:INBOX",
714 "file:/home/shannon/mail/foo",
715 "/tmp/foo",
716 "//host/tmp/foo",
717 ":/tmp/foo",
718 "/really/weird:/tmp/foo#bar",
719 ""
720 };
721
722 URLName url =
723 new URLName("protocol", "host", 119, "file", "userid", "password");
724 System.out.println("Test URL: " + url.toString());
725 if (argv.length == 0) {
726 for (int i = 0; i < testURLNames.length; i++) {
727 print(testURLNames[i]);
728 System.out.println();
729 }
730 } else {
731 for (int i = 0; i < argv.length; i++) {
732 print(argv[i]);
733 System.out.println();
734 }
735 if (argv.length == 2) {
736 URLName u1 = new URLName(argv[0]);
737 URLName u2 = new URLName(argv[1]);
738 System.out.println("URL1 hash code: " + u1.hashCode());
739 System.out.println("URL2 hash code: " + u2.hashCode());
740 if (u1.equals(u2))
741 System.out.println("success, equal");
742 else
743 System.out.println("fail, not equal");
744 if (u2.equals(u1))
745 System.out.println("success, equal");
746 else
747 System.out.println("fail, not equal");
748 if (u1.hashCode() == u2.hashCode())
749 System.out.println("success, hashCodes equal");
750 else
751 System.out.println("fail, hashCodes not equal");
752 }
753 }
754 }
755
756 private static void print(String name) {
757 URLName url = new URLName(name);
758 System.out.println("Original URL: " + name);
759 System.out.println("The fullUrl : " + url.toString());
760 if (!name.equals(url.toString()))
761 System.out.println(" : NOT EQUAL!");
762 System.out.println("The protocol is: " + url.getProtocol());
763 System.out.println("The host is: " + url.getHost());
764 System.out.println("The port is: " + url.getPort());
765 System.out.println("The user is: " + url.getUsername());
766 System.out.println("The password is: " + url.getPassword());
767 System.out.println("The file is: " + url.getFile());
768 System.out.println("The ref is: " + url.getRef());
769 }
770 */
771 }