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