PageRenderTime 47ms CodeModel.GetById 8ms RepoModel.GetById 0ms app.codeStats 1ms

/src/sys/java/fan/sys/Uri.java

https://bitbucket.org/bedlaczech/fan-1.0
Java | 1487 lines | 1145 code | 197 blank | 145 comment | 400 complexity | faae1e2df7a6abda9a2f08c185acb954 MD5 | raw file
Possible License(s): CC-BY-SA-3.0
  1. //
  2. // Copyright (c) 2006, Brian Frank and Andy Frank
  3. // Licensed under the Academic Free License version 3.0
  4. //
  5. // History:
  6. // 28 Jun 06 Brian Frank Creation
  7. // 01 Aug 07 Brian Rewrite with our own parser/encoder
  8. //
  9. package fan.sys;
  10. import fanx.serial.*;
  11. import fanx.util.*;
  12. /**
  13. * Uri is used to immutably represent a Universal Resource Identifier.
  14. */
  15. public final class Uri
  16. extends FanObj
  17. implements Literal
  18. {
  19. //////////////////////////////////////////////////////////////////////////
  20. // Construction
  21. //////////////////////////////////////////////////////////////////////////
  22. public static Uri fromStr(String s) { return fromStr(s, true); }
  23. public static Uri fromStr(String s, boolean checked)
  24. {
  25. try
  26. {
  27. return new Uri(new Decoder(s, false).decode());
  28. }
  29. catch (ParseErr e)
  30. {
  31. if (!checked) return null;
  32. throw ParseErr.make("Uri", s, e.msg());
  33. }
  34. catch (Exception e)
  35. {
  36. if (!checked) return null;
  37. throw ParseErr.make("Uri", s);
  38. }
  39. }
  40. public static Uri decode(String s) { return decode(s, true); }
  41. public static Uri decode(String s, boolean checked)
  42. {
  43. try
  44. {
  45. return new Uri(new Decoder(s, true).decode());
  46. }
  47. catch (ParseErr e)
  48. {
  49. if (!checked) return null;
  50. throw ParseErr.make("Uri", s, e.msg());
  51. }
  52. catch (Exception e)
  53. {
  54. if (!checked) return null;
  55. throw ParseErr.make("Uri", s);
  56. }
  57. }
  58. //////////////////////////////////////////////////////////////////////////
  59. // Utils
  60. //////////////////////////////////////////////////////////////////////////
  61. public static Map decodeQuery(String s)
  62. {
  63. try
  64. {
  65. return new Decoder(s, true).decodeQuery();
  66. }
  67. catch (ArgErr e)
  68. {
  69. throw ArgErr.make("Invalid Uri query: `" + s + "`: " + e.msg());
  70. }
  71. catch (Exception e)
  72. {
  73. throw ArgErr.make("Invalid Uri query: `" + s + "`");
  74. }
  75. }
  76. public static String encodeQuery(Map map)
  77. {
  78. StringBuilder buf = new StringBuilder(256);
  79. java.util.Iterator it = map.keysIterator();
  80. while (it.hasNext())
  81. {
  82. String key = (String)it.next();
  83. String val = (String)map.get(key);
  84. if (buf.length() > 0) buf.append('&');
  85. encodeQueryStr(buf, key);
  86. if (val != null)
  87. {
  88. buf.append('=');
  89. encodeQueryStr(buf, val);
  90. }
  91. }
  92. return buf.toString();
  93. }
  94. static void encodeQueryStr(StringBuilder buf, String str)
  95. {
  96. for (int i=0; i<str.length(); ++i)
  97. {
  98. int c = str.charAt(i);
  99. if (c < 128 && (charMap[c] & QUERY) != 0 && (delimEscMap[c] & QUERY) == 0)
  100. buf.append((char)c);
  101. else if (c == ' ')
  102. buf.append('+');
  103. else
  104. percentEncodeChar(buf, c);
  105. }
  106. }
  107. //////////////////////////////////////////////////////////////////////////
  108. // Java Constructors
  109. //////////////////////////////////////////////////////////////////////////
  110. private Uri(Sections x)
  111. {
  112. scheme = x.scheme;
  113. userInfo = x.userInfo;
  114. host = x.host;
  115. port = x.port;
  116. pathStr = x.pathStr;
  117. path = x.path.ro();
  118. queryStr = x.queryStr;
  119. query = x.query.ro();
  120. frag = x.frag;
  121. str = x.str != null ? x.str : new Encoder(this, false).encode();
  122. }
  123. //////////////////////////////////////////////////////////////////////////
  124. // Sections
  125. //////////////////////////////////////////////////////////////////////////
  126. static class Sections
  127. {
  128. void setAuth(Uri x) { userInfo = x.userInfo; host = x.host; port = x.port; }
  129. void setPath(Uri x) { pathStr = x.pathStr; path = x.path; }
  130. void setQuery(Uri x) { queryStr = x.queryStr; query = x.query; }
  131. void setFrag(Uri x) { frag = x.frag; }
  132. void normalize()
  133. {
  134. normalizeSchemes();
  135. normalizePath();
  136. normalizeQuery();
  137. }
  138. private void normalizeSchemes()
  139. {
  140. if (scheme == null) return;
  141. if (scheme.equals("http")) { normalizeScheme(80); return; }
  142. if (scheme.equals("https")) { normalizeScheme(443); return; }
  143. if (scheme.equals("ftp")) { normalizeScheme(21); return; }
  144. }
  145. private void normalizeScheme(int p)
  146. {
  147. // port -> null
  148. if (port != null && port.longValue() == p) port = null;
  149. // if path is "" -> "/"
  150. if (pathStr == null || pathStr.length() == 0)
  151. {
  152. pathStr = "/";
  153. if (path == null) path = emptyPath();
  154. }
  155. }
  156. private void normalizePath()
  157. {
  158. if (path == null) return;
  159. boolean isAbs = pathStr.startsWith("/");
  160. boolean isDir = pathStr.endsWith("/");
  161. boolean dotLast = false;
  162. boolean modified = false;
  163. for (int i=0; i<path.sz(); ++i)
  164. {
  165. String seg = (String)path.get(i);
  166. if (seg.equals(".") && (path.sz() > 1 || host != null))
  167. {
  168. path.removeAt(i);
  169. modified = true;
  170. dotLast = true;
  171. i -= 1;
  172. }
  173. else if (seg.equals("..") && i > 0 && !path.get(i-1).toString().equals(".."))
  174. {
  175. path.removeAt(i);
  176. path.removeAt(i-1);
  177. modified = true;
  178. i -= 2;
  179. dotLast = true;
  180. }
  181. else
  182. {
  183. dotLast = false;
  184. }
  185. }
  186. if (modified)
  187. {
  188. if (dotLast) isDir = true;
  189. if (path.sz() == 0 || path.last().toString().equals("..")) isDir = false;
  190. pathStr = toPathStr(isAbs, path, isDir);
  191. }
  192. }
  193. private void normalizeQuery()
  194. {
  195. if (query == null)
  196. query = emptyQuery();
  197. }
  198. String scheme;
  199. String host;
  200. String userInfo;
  201. Long port;
  202. String pathStr;
  203. List path;
  204. String queryStr;
  205. Map query;
  206. String frag;
  207. String str;
  208. }
  209. //////////////////////////////////////////////////////////////////////////
  210. // Decoder
  211. //////////////////////////////////////////////////////////////////////////
  212. static class Decoder extends Sections
  213. {
  214. Decoder(String str, boolean decoding)
  215. {
  216. this.str = str;
  217. this.decoding = decoding;
  218. }
  219. Decoder decode()
  220. {
  221. String str = this.str;
  222. int len = str.length();
  223. int pos = 0;
  224. // ==== scheme ====
  225. // scan the string from the beginning looking for either a
  226. // colon or any character which doesn't fit a valid scheme
  227. boolean hasUpper = false;
  228. for (int i=0; i<len; ++i)
  229. {
  230. int c = str.charAt(i);
  231. if (isScheme(c)) { hasUpper |= isUpper(c); continue; }
  232. if (c != ':') break;
  233. // at this point we have a scheme; if we detected
  234. // any upper case characters normalize to lowercase
  235. pos = i + 1;
  236. String scheme = str.substring(0, i);
  237. if (hasUpper) scheme = FanStr.lower(scheme);
  238. this.scheme = scheme;
  239. break;
  240. }
  241. // ==== authority ====
  242. // authority must start with //
  243. if (pos+1 < len && str.charAt(pos) == '/' && str.charAt(pos+1) == '/')
  244. {
  245. // find end of authority which is /, ?, #, or end of string;
  246. // while we're scanning look for @ and last colon which isn't
  247. // inside an [] IPv6 literal
  248. int authStart = pos+2, authEnd = len, at = -1, colon = -1;
  249. for (int i=authStart; i<len; ++i)
  250. {
  251. int c = str.charAt(i);
  252. if (c == '/' || c == '?' || c == '#') { authEnd = i; break; }
  253. else if (c == '@' && at < 0) { at = i; colon = -1; }
  254. else if (c == ':') colon = i;
  255. else if (c == ']') colon = -1;
  256. }
  257. // start with assumption that there is no userinfo or port
  258. int hostStart = authStart, hostEnd = authEnd;
  259. // if we found an @ symbol, parse out userinfo
  260. if (at > 0)
  261. {
  262. this.userInfo = substring(authStart, at, USER);
  263. hostStart = at+1;
  264. }
  265. // if we found an colon, parse out port
  266. if (colon > 0)
  267. {
  268. this.port = Long.valueOf(Integer.parseInt(str.substring(colon+1, authEnd)));
  269. hostEnd = colon;
  270. }
  271. // host is everything left in the authority
  272. this.host = substring(hostStart, hostEnd, HOST);
  273. pos = authEnd;
  274. }
  275. // ==== path ====
  276. // scan the string looking '?' or '#' which ends the path
  277. // section; while we're scanning count the number of slashes
  278. int pathStart = pos, pathEnd = len, numSegs = 1, prev = 0;
  279. for (int i=pathStart; i<len; ++i)
  280. {
  281. int c = str.charAt(i);
  282. if (prev != '\\')
  283. {
  284. if (c == '?' || c == '#') { pathEnd = i; break; }
  285. if (i != pathStart && c == '/') ++numSegs;
  286. prev = c;
  287. }
  288. else
  289. {
  290. prev = (c != '\\') ? c : 0;
  291. }
  292. }
  293. // we now have the complete path section
  294. this.pathStr = substring(pathStart, pathEnd, PATH);
  295. this.path = pathSegments(pathStr, numSegs);
  296. pos = pathEnd;
  297. // ==== query ====
  298. if (pos < len && str.charAt(pos) == '?')
  299. {
  300. // look for end of query which is # or end of string
  301. int queryStart = pos+1, queryEnd = len; prev = 0;
  302. for (int i=queryStart; i<len; ++i)
  303. {
  304. int c = str.charAt(i);
  305. if (prev != '\\')
  306. {
  307. if (c == '#') { queryEnd = i; break; }
  308. prev = c;
  309. }
  310. else
  311. {
  312. prev = (c != '\\') ? c : 0;
  313. }
  314. }
  315. // we now have the complete query section
  316. this.queryStr = substring(queryStart, queryEnd, QUERY);
  317. this.query = parseQuery(queryStr);
  318. pos = queryEnd;
  319. }
  320. // ==== frag ====
  321. if (pos < len && str.charAt(pos) == '#')
  322. {
  323. this.frag = substring(pos+1, len, FRAG);
  324. }
  325. // === normalize ===
  326. normalize();
  327. // if decoding, then we don't want to use original
  328. // str as Uri.str, so null it out
  329. this.str = null;
  330. return this;
  331. }
  332. private List pathSegments(String pathStr, int numSegs)
  333. {
  334. // if pathStr is "/" then path si the empty list
  335. int len = pathStr.length();
  336. if (len == 0 || (len == 1 && pathStr.charAt(0) == '/'))
  337. return emptyPath();
  338. // check for trailing slash
  339. if (len > 1 && pathStr.charAt(len-1) == '/')
  340. {
  341. numSegs--;
  342. len--;
  343. }
  344. // parse the segments
  345. String[] path = new String[numSegs];
  346. int n = 0;
  347. int segStart = 0, prev = 0;
  348. for (int i=0; i<pathStr.length(); ++i)
  349. {
  350. int c = pathStr.charAt(i);
  351. if (prev != '\\')
  352. {
  353. if (c == '/')
  354. {
  355. if (i > 0) path[n++] = pathStr.substring(segStart, i);
  356. segStart = i+1;
  357. }
  358. prev = c;
  359. }
  360. else
  361. {
  362. prev = (c != '\\') ? c : 0;
  363. }
  364. }
  365. if (segStart < len)
  366. path[n++] = pathStr.substring(segStart, pathStr.length());
  367. return new List(Sys.StrType, path);
  368. }
  369. Map decodeQuery()
  370. {
  371. return parseQuery(substring(0, str.length(), QUERY));
  372. }
  373. private Map parseQuery(String q)
  374. {
  375. if (q == null) return null;
  376. Map map = new Map(Sys.StrType, Sys.StrType);
  377. try
  378. {
  379. int start = 0, eq = 0, len = q.length(), prev = 0;
  380. boolean escaped = false;
  381. for (int i=0; i<len; ++i)
  382. {
  383. int ch = q.charAt(i);
  384. if (prev != '\\')
  385. {
  386. if (ch == '=') eq = i;
  387. if (ch != '&' && ch != ';') { prev = ch; continue; }
  388. }
  389. else
  390. {
  391. escaped = true;
  392. prev = (ch != '\\') ? ch : 0;
  393. continue;
  394. }
  395. if (start < i)
  396. {
  397. addQueryParam(map, q, start, eq, i, escaped);
  398. escaped = false;
  399. }
  400. start = eq = i+1;
  401. }
  402. if (start < len)
  403. addQueryParam(map, q, start, eq, len, escaped);
  404. }
  405. catch (Exception e)
  406. {
  407. // don't let internal error bring down whole uri
  408. e.printStackTrace();
  409. }
  410. return map;
  411. }
  412. private void addQueryParam(Map map, String q, int start, int eq, int end, boolean escaped)
  413. {
  414. String key, val;
  415. if (start == eq && q.charAt(start) != '=')
  416. {
  417. key = toQueryStr(q, start, end, escaped);
  418. val = "true";
  419. }
  420. else
  421. {
  422. key = toQueryStr(q, start, eq, escaped);
  423. val = toQueryStr(q, eq+1, end, escaped);
  424. }
  425. String dup = (String)map.get(key);
  426. if (dup != null) val = dup + "," + val;
  427. map.set(key, val);
  428. }
  429. private String toQueryStr(String q, int start, int end, boolean escaped)
  430. {
  431. if (!escaped) return q.substring(start, end);
  432. StringBuilder s = new StringBuilder(end-start);
  433. int prev = 0;
  434. for (int i=start; i<end; ++i)
  435. {
  436. int c = q.charAt(i);
  437. if (c != '\\')
  438. {
  439. s.append((char)c);
  440. prev = c;
  441. }
  442. else
  443. {
  444. if (prev == '\\') { s.append((char)c); prev = 0; }
  445. else prev = c;
  446. }
  447. }
  448. return s.toString();
  449. }
  450. private String substring(int start, int end, int section)
  451. {
  452. if (!decoding) return str.substring(start, end);
  453. StringBuilder buf = new StringBuilder(end-start);
  454. dpos = start;
  455. while (dpos < end)
  456. {
  457. int ch = nextChar(section);
  458. if (nextCharWasEscaped && ch < delimEscMap.length && (delimEscMap[ch] & section) != 0)
  459. buf.append('\\');
  460. buf.append((char)ch);
  461. }
  462. return buf.toString();
  463. }
  464. private int nextChar(int section)
  465. {
  466. int c = nextOctet(section);
  467. if (c < 0) return -1;
  468. int c2, c3;
  469. switch (c >> 4)
  470. {
  471. case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
  472. /* 0xxxxxxx*/
  473. return c;
  474. case 12: case 13:
  475. /* 110x xxxx 10xx xxxx*/
  476. c2 = nextOctet(section);
  477. if ((c2 & 0xC0) != 0x80)
  478. throw err("Invalid UTF-8 encoding");
  479. return ((c & 0x1F) << 6) | (c2 & 0x3F);
  480. case 14:
  481. /* 1110 xxxx 10xx xxxx 10xx xxxx */
  482. c2 = nextOctet(section);
  483. c3 = nextOctet(section);
  484. if (((c2 & 0xC0) != 0x80) || ((c3 & 0xC0) != 0x80))
  485. throw err("Invalid UTF-8 encoding");
  486. return (((c & 0x0F) << 12) | ((c2 & 0x3F) << 6) | ((c3 & 0x3F) << 0));
  487. default:
  488. throw err("Invalid UTF-8 encoding");
  489. }
  490. }
  491. private int nextOctet(int section)
  492. {
  493. int c = str.charAt(dpos++);
  494. // if percent encoded applied to all sections except
  495. // scheme which should never never use this method
  496. if (c == '%')
  497. {
  498. nextCharWasEscaped = true;
  499. return (hexNibble(str.charAt(dpos++)) << 4) | hexNibble(str.charAt(dpos++));
  500. }
  501. else
  502. {
  503. nextCharWasEscaped = false;
  504. }
  505. // + maps to space only in query
  506. if (c == '+' && section == QUERY)
  507. return ' ';
  508. // verify character ok
  509. if (c >= charMap.length || (charMap[c] & section) == 0)
  510. throw err("Invalid char in " + toSection(section) + " at index " + (dpos-1));
  511. // return character as is
  512. return c;
  513. }
  514. boolean decoding;
  515. int dpos;
  516. boolean nextCharWasEscaped;
  517. }
  518. //////////////////////////////////////////////////////////////////////////
  519. // Encoder
  520. //////////////////////////////////////////////////////////////////////////
  521. static class Encoder
  522. {
  523. Encoder(Uri uri, boolean encoding)
  524. {
  525. this.uri = uri;
  526. this.encoding = encoding;
  527. this.buf = new StringBuilder();
  528. }
  529. String encode()
  530. {
  531. Uri uri = this.uri;
  532. StringBuilder buf = this.buf;
  533. // scheme
  534. if (uri.scheme != null) buf.append(uri.scheme).append(':');
  535. // authority
  536. if (uri.userInfo != null || uri.host != null || uri.port != null)
  537. {
  538. buf.append('/').append('/');
  539. if (uri.userInfo != null) encode(uri.userInfo, USER).append('@');
  540. if (uri.host != null) encode(uri.host, HOST);
  541. if (uri.port != null) buf.append(':').append(uri.port.longValue());
  542. }
  543. // path
  544. if (uri.pathStr != null)
  545. encode(uri.pathStr, PATH);
  546. // query
  547. if (uri.queryStr != null)
  548. { buf.append('?'); encode(uri.queryStr, QUERY); }
  549. // frag
  550. if (uri.frag != null)
  551. { buf.append('#'); encode(uri.frag, FRAG); }
  552. return buf.toString();
  553. }
  554. StringBuilder encode(String s, int section)
  555. {
  556. if (!encoding) return buf.append(s);
  557. StringBuilder buf = this.buf;
  558. int len = s.length();
  559. int c = 0, prev;
  560. for (int i=0; i<len; ++i)
  561. {
  562. prev = c;
  563. c = s.charAt(i);
  564. // unreserved character
  565. if (c < 128 && (charMap[c] & section) != 0 && prev != '\\')
  566. {
  567. buf.append((char)c);
  568. continue;
  569. }
  570. // the backslash esc itself doesn't get encoded
  571. if (c == '\\' && prev != '\\') continue;
  572. // we have a reserved, escaped, or non-ASCII
  573. // encode
  574. if (c == ' ' && section == QUERY)
  575. buf.append('+');
  576. else
  577. percentEncodeChar(buf, c);
  578. // if we just encoded backslash, then it
  579. // doesn't escape the next char
  580. if (c == '\\') c = 0;
  581. }
  582. return buf;
  583. }
  584. Uri uri;
  585. boolean encoding;
  586. StringBuilder buf;
  587. }
  588. static void percentEncodeChar(StringBuilder buf, int c)
  589. {
  590. if (c <= 0x007F)
  591. {
  592. percentEncodeByte(buf, c);
  593. }
  594. else if (c > 0x07FF)
  595. {
  596. percentEncodeByte(buf, 0xE0 | ((c >> 12) & 0x0F));
  597. percentEncodeByte(buf, 0x80 | ((c >> 6) & 0x3F));
  598. percentEncodeByte(buf, 0x80 | ((c >> 0) & 0x3F));
  599. }
  600. else
  601. {
  602. percentEncodeByte(buf, 0xC0 | ((c >> 6) & 0x1F));
  603. percentEncodeByte(buf, 0x80 | ((c >> 0) & 0x3F));
  604. }
  605. }
  606. static void percentEncodeByte(StringBuilder buf, int c)
  607. {
  608. buf.append('%');
  609. int hi = (c >> 4) & 0xf;
  610. int lo = c & 0xf;
  611. buf.append((char)(hi < 10 ? '0'+hi : 'A'+(hi-10)));
  612. buf.append((char)(lo < 10 ? '0'+lo : 'A'+(lo-10)));
  613. }
  614. //////////////////////////////////////////////////////////////////////////
  615. // Identity
  616. //////////////////////////////////////////////////////////////////////////
  617. public final boolean equals(Object obj)
  618. {
  619. if (obj instanceof Uri)
  620. {
  621. return str.equals(((Uri)obj).str);
  622. }
  623. return false;
  624. }
  625. public int hashCode()
  626. {
  627. return str.hashCode();
  628. }
  629. public long hash()
  630. {
  631. return FanStr.hash(str);
  632. }
  633. public String toStr()
  634. {
  635. return str;
  636. }
  637. public String toLocale()
  638. {
  639. return str;
  640. }
  641. public void encode(ObjEncoder out)
  642. {
  643. out.wStrLiteral(str, '`');
  644. }
  645. public Type typeof()
  646. {
  647. return Sys.UriType;
  648. }
  649. public String encode()
  650. {
  651. String x = encoded;
  652. if (x != null) return x;
  653. return encoded = new Encoder(this, true).encode();
  654. }
  655. //////////////////////////////////////////////////////////////////////////
  656. // Components
  657. //////////////////////////////////////////////////////////////////////////
  658. public boolean isAbs()
  659. {
  660. return scheme != null;
  661. }
  662. public boolean isRel()
  663. {
  664. return scheme == null;
  665. }
  666. public boolean isDir()
  667. {
  668. if (pathStr != null)
  669. {
  670. String p = pathStr;
  671. int len = p.length();
  672. if (len > 0 && p.charAt(len-1) == '/')
  673. return true;
  674. }
  675. return false;
  676. }
  677. public String scheme()
  678. {
  679. return scheme;
  680. }
  681. public String auth()
  682. {
  683. if (host == null) return null;
  684. if (port == null)
  685. {
  686. if (userInfo == null) return host;
  687. else return userInfo + '@' + host;
  688. }
  689. else
  690. {
  691. if (userInfo == null) return host + ':' + port;
  692. else return userInfo + '@' + host + ':' + port;
  693. }
  694. }
  695. public String host()
  696. {
  697. return host;
  698. }
  699. public String userInfo()
  700. {
  701. return userInfo;
  702. }
  703. public Long port()
  704. {
  705. return port;
  706. }
  707. public final String path(int depth) { return (String)path.get(depth); }
  708. public List path()
  709. {
  710. return path;
  711. }
  712. public String pathStr()
  713. {
  714. return pathStr;
  715. }
  716. public boolean isPathAbs()
  717. {
  718. if (pathStr == null || pathStr.length() == 0)
  719. return false;
  720. else
  721. return pathStr.charAt(0) == '/';
  722. }
  723. public boolean isPathOnly()
  724. {
  725. return scheme == null && host == null && port == null &&
  726. userInfo == null && queryStr == null && frag == null;
  727. }
  728. public String name()
  729. {
  730. if (path.sz() == 0) return "";
  731. return (String)path.last();
  732. }
  733. public String basename()
  734. {
  735. String n = name();
  736. int dot = n.lastIndexOf('.');
  737. if (dot < 2)
  738. {
  739. if (dot < 0) return n;
  740. if (n.equals(".")) return n;
  741. if (n.equals("..")) return n;
  742. }
  743. return n.substring(0, dot);
  744. }
  745. public String ext()
  746. {
  747. String n = name();
  748. int dot = n.lastIndexOf('.');
  749. if (dot < 2)
  750. {
  751. if (dot < 0) return null;
  752. if (n.equals(".")) return null;
  753. if (n.equals("..")) return null;
  754. }
  755. return n.substring(dot+1);
  756. }
  757. public MimeType mimeType()
  758. {
  759. if (isDir()) return MimeType.dir;
  760. return MimeType.forExt(ext());
  761. }
  762. public Map query()
  763. {
  764. return query;
  765. }
  766. public String queryStr()
  767. {
  768. return queryStr;
  769. }
  770. public String frag()
  771. {
  772. return frag;
  773. }
  774. //////////////////////////////////////////////////////////////////////////
  775. // Utils
  776. //////////////////////////////////////////////////////////////////////////
  777. public Uri parent()
  778. {
  779. // if no path bail
  780. if (path.sz() == 0) return null;
  781. // if just a simple filename, then no parent
  782. if (path.sz() == 1 && !isPathAbs() && !isDir()) return null;
  783. // use slice
  784. return slice(parentRange, false);
  785. }
  786. public Uri pathOnly()
  787. {
  788. if (pathStr == null)
  789. throw Err.make("Uri has no path: " + this);
  790. if (scheme == null && userInfo == null && host == null &&
  791. port == null && queryStr == null && frag == null)
  792. return this;
  793. Sections t = new Sections();
  794. t.path = this.path;
  795. t.pathStr = this.pathStr;
  796. t.query = emptyQuery();
  797. t.str = this.pathStr;
  798. return new Uri(t);
  799. }
  800. public Uri getRange(Range range) { return slice(range, false); }
  801. public Uri getRangeToPathAbs(Range range) { return slice(range, true); }
  802. private Uri slice(Range range, boolean forcePathAbs)
  803. {
  804. if (pathStr == null)
  805. throw Err.make("Uri has no path: " + this);
  806. int size = path.sz();
  807. int s = range.startIndex(size);
  808. int e = range.endIndex(size);
  809. int n = e - s + 1;
  810. if (n < 0) throw IndexErr.make(range);
  811. boolean head = (s == 0);
  812. boolean tail = (e == size-1);
  813. if (head && tail && (!forcePathAbs || isPathAbs())) return this;
  814. Sections t = new Sections();
  815. t.path = path.getRange(range);
  816. StringBuilder sb = new StringBuilder(pathStr.length());
  817. if ((head && isPathAbs()) || forcePathAbs) sb.append('/');
  818. for (int i=0; i<t.path.sz(); ++i)
  819. {
  820. if (i > 0) sb.append('/');
  821. sb.append(t.path.get(i));
  822. }
  823. if (t.path.sz() > 0 && (!tail || isDir())) sb.append('/');
  824. t.pathStr = sb.toString();
  825. if (head)
  826. {
  827. t.scheme = scheme;
  828. t.userInfo = userInfo;
  829. t.host = host;
  830. t.port = port;
  831. }
  832. if (tail)
  833. {
  834. t.queryStr = queryStr;
  835. t.query = query;
  836. t.frag = frag;
  837. }
  838. else
  839. {
  840. t.query = emptyQuery();
  841. }
  842. if (!head && !tail)
  843. {
  844. t.str = t.pathStr;
  845. }
  846. return new Uri(t);
  847. }
  848. //////////////////////////////////////////////////////////////////////////
  849. // Relativize
  850. //////////////////////////////////////////////////////////////////////////
  851. public Uri relTo(Uri base)
  852. {
  853. if (!OpUtil.compareEQ(this.scheme, base.scheme) ||
  854. !OpUtil.compareEQ(this.userInfo, base.userInfo) ||
  855. !OpUtil.compareEQ(this.host, base.host) ||
  856. !OpUtil.compareEQ(this.port, base.port))
  857. return this;
  858. // at this point we know we have the same scheme and auth, and
  859. // we're going to create a new URI which is a subset of this one
  860. Sections t = new Sections();
  861. t.query = this.query;
  862. t.queryStr = this.queryStr;
  863. t.frag = this.frag;
  864. // find divergence
  865. int d=0;
  866. int len = Math.min(this.path.sz(), base.path.sz());
  867. for (; d<len; ++d)
  868. if (!this.path.get(d).equals(base.path.get(d)))
  869. break;
  870. // if diverenge is at root, then no commonality
  871. if (d == 0)
  872. {
  873. // `/a/b/c`.relTo(`/`) should be `a/b/c`
  874. if (base.path.isEmpty() && this.pathStr.startsWith("/"))
  875. {
  876. t.path = this.path;
  877. t.pathStr = this.pathStr.substring(1);
  878. }
  879. else
  880. {
  881. t.path = this.path;
  882. t.pathStr = this.pathStr;
  883. }
  884. }
  885. // if paths are exactly the same
  886. else if (d == this.path.sz() && d == base.path.sz())
  887. {
  888. t.path = emptyPath();
  889. t.pathStr = "";
  890. }
  891. // create sub-path at divergence point
  892. else
  893. {
  894. // slice my path
  895. t.path = this.path.getRange(Range.makeInclusive(d, -1));
  896. // insert .. backup if needed
  897. int backup = base.path.sz() - d;
  898. if (!base.isDir()) backup--;
  899. while (backup-- > 0) t.path.insert(0L, "..");
  900. // format the new path string
  901. t.pathStr = toPathStr(false, t.path, this.isDir());
  902. }
  903. return new Uri(t);
  904. }
  905. public Uri relToAuth()
  906. {
  907. if (scheme == null && userInfo == null &&
  908. host == null && port == null)
  909. return this;
  910. Sections t = new Sections();
  911. t.path = this.path;
  912. t.pathStr = this.pathStr;
  913. t.query = this.query;
  914. t.queryStr = this.queryStr;
  915. t.frag = this.frag;
  916. return new Uri(t);
  917. }
  918. //////////////////////////////////////////////////////////////////////////
  919. // Plus
  920. //////////////////////////////////////////////////////////////////////////
  921. public Uri plus(Uri r)
  922. {
  923. // if r is more or equal as absolute as base, return r
  924. if (r.scheme != null) return r;
  925. if (r.host != null && this.scheme == null) return r;
  926. if (r.isPathAbs() && this.host == null) return r;
  927. // this algorthm is lifted straight from
  928. // RFC 3986 (5.2.2) Transform References;
  929. Uri base = this;
  930. Sections t = new Sections();
  931. if (r.host != null)
  932. {
  933. t.setAuth(r);
  934. t.setPath(r);
  935. t.setQuery(r);
  936. }
  937. else
  938. {
  939. if (r.pathStr == null || r.pathStr.equals(""))
  940. {
  941. t.setPath(base);
  942. if (r.queryStr != null)
  943. t.setQuery(r);
  944. else
  945. t.setQuery(base);
  946. }
  947. else
  948. {
  949. if (r.pathStr.startsWith("/"))
  950. t.setPath(r);
  951. else
  952. merge(t, base, r);
  953. t.setQuery(r);
  954. }
  955. t.setAuth(base);
  956. }
  957. t.scheme = base.scheme;
  958. t.frag = r.frag;
  959. t.normalize();
  960. return new Uri(t);
  961. }
  962. static void merge(Sections t, Uri base, Uri r)
  963. {
  964. boolean baseIsAbs = base.isPathAbs();
  965. boolean baseIsDir = base.isDir();
  966. boolean rIsDir = r.isDir();
  967. List rPath = r.path;
  968. boolean dotLast = false;
  969. // compute the target path taking into account whether
  970. // the base is a dir and any dot segments in relative ref
  971. List tPath;
  972. if (base.path.sz() == 0)
  973. {
  974. tPath = r.path;
  975. }
  976. else
  977. {
  978. tPath = base.path.rw();
  979. if (!baseIsDir) tPath.pop();
  980. for (int i=0; i<rPath.sz(); ++i)
  981. {
  982. String rSeg = (String)rPath.get(i);
  983. if (rSeg.equals(".")) { dotLast = true; continue; }
  984. if (rSeg.equals(".."))
  985. {
  986. if (!tPath.isEmpty()) { tPath.pop(); dotLast = true; continue; }
  987. if (baseIsAbs) continue;
  988. }
  989. tPath.add(rSeg); dotLast = false;
  990. }
  991. }
  992. t.path = tPath;
  993. t.pathStr = toPathStr(baseIsAbs, tPath, rIsDir || dotLast);
  994. }
  995. static String toPathStr(boolean isAbs, List path, boolean isDir)
  996. {
  997. StringBuilder buf = new StringBuilder();
  998. if (isAbs) buf.append('/');
  999. for (int i=0; i<path.sz(); ++i)
  1000. {
  1001. if (i > 0) buf.append('/');
  1002. buf.append(path.get(i));
  1003. }
  1004. if (isDir && !(buf.length() > 0 && buf.charAt(buf.length()-1) == '/'))
  1005. buf.append('/');
  1006. return buf.toString();
  1007. }
  1008. public Uri plusName(String name) { return plusName(name, false); }
  1009. public Uri plusName(String name, boolean asDir)
  1010. {
  1011. int size = path.sz();
  1012. boolean isDir = isDir() || path.isEmpty();
  1013. int newSize = isDir ? size + 1 : size;
  1014. String[] temp = (String[])path.toArray(new String[newSize]);
  1015. temp[newSize-1] = name;
  1016. Sections t = new Sections();
  1017. t.scheme = this.scheme;
  1018. t.userInfo = this.userInfo;
  1019. t.host = this.host;
  1020. t.port = this.port;
  1021. t.query = emptyQuery();
  1022. t.queryStr = null;
  1023. t.frag = null;
  1024. t.path = new List(Sys.StrType, temp);
  1025. t.pathStr = toPathStr(isAbs() || isPathAbs(), t.path, asDir);
  1026. return new Uri(t);
  1027. }
  1028. public Uri plusSlash()
  1029. {
  1030. if (isDir()) return this;
  1031. Sections t = new Sections();
  1032. t.scheme = this.scheme;
  1033. t.userInfo = this.userInfo;
  1034. t.host = this.host;
  1035. t.port = this.port;
  1036. t.query = this.query;
  1037. t.queryStr = this.queryStr;
  1038. t.frag = this.frag;
  1039. t.path = this.path;
  1040. t.pathStr = this.pathStr + "/";
  1041. return new Uri(t);
  1042. }
  1043. public Uri plusQuery(Map q)
  1044. {
  1045. if (q == null || q.isEmpty()) return this;
  1046. Map merge = this.query.dup().setAll(q);
  1047. StringBuilder s = new StringBuilder(256);
  1048. java.util.Iterator it = merge.pairsIterator();
  1049. while (it.hasNext())
  1050. {
  1051. if (s.length() > 0) s.append('&');
  1052. java.util.Map.Entry e = (java.util.Map.Entry)it.next();
  1053. String key = (String)e.getKey();
  1054. String val = (String)e.getValue();
  1055. appendQueryStr(s, key);
  1056. s.append('=');
  1057. appendQueryStr(s, val);
  1058. }
  1059. Sections t = new Sections();
  1060. t.scheme = this.scheme;
  1061. t.userInfo = this.userInfo;
  1062. t.host = this.host;
  1063. t.port = this.port;
  1064. t.frag = this.frag;
  1065. t.pathStr = this.pathStr;
  1066. t.path = this.path;
  1067. t.query = merge.ro();
  1068. t.queryStr = s.toString();
  1069. return new Uri(t);
  1070. }
  1071. static void appendQueryStr(StringBuilder buf, String str)
  1072. {
  1073. for (int i=0; i<str.length(); ++i)
  1074. {
  1075. int c = str.charAt(i);
  1076. if (c < delimEscMap.length && (delimEscMap[c] & QUERY) != 0)
  1077. buf.append('\\');
  1078. buf.append((char)c);
  1079. }
  1080. }
  1081. //////////////////////////////////////////////////////////////////////////
  1082. // Resolution
  1083. //////////////////////////////////////////////////////////////////////////
  1084. public File toFile()
  1085. {
  1086. return File.make(this);
  1087. }
  1088. public Object get() { return get(null, true); }
  1089. public Object get(Object base) { return get(base, true); }
  1090. public Object get(Object base, boolean checked)
  1091. {
  1092. // if we have a relative uri, we need to resolve against
  1093. // the base object's uri
  1094. Uri uri = this;
  1095. if (scheme == null)
  1096. {
  1097. if (base == null) throw UnresolvedErr.make("Relative uri with no base: " + this);
  1098. Uri baseUri = null;
  1099. try
  1100. {
  1101. baseUri = (Uri)trap(base, "uri", null);
  1102. if (baseUri == null)
  1103. throw UnresolvedErr.make("Base object's uri is null: " + this);
  1104. }
  1105. catch (Throwable e)
  1106. {
  1107. throw UnresolvedErr.make("Cannot access base '" + FanObj.typeof(base) + ".uri' to normalize: " + this, e);
  1108. }
  1109. if (baseUri.scheme == null)
  1110. throw UnresolvedErr.make("Base object's uri is not absolute: " + baseUri);
  1111. uri = baseUri.plus(this);
  1112. }
  1113. // resolve scheme handler
  1114. UriScheme scheme = UriScheme.find(uri.scheme);
  1115. // route to scheme
  1116. try
  1117. {
  1118. return scheme.get(uri, base);
  1119. }
  1120. catch (UnresolvedErr e)
  1121. {
  1122. if (checked) throw e;
  1123. return null;
  1124. }
  1125. }
  1126. //////////////////////////////////////////////////////////////////////////
  1127. // Conversion
  1128. //////////////////////////////////////////////////////////////////////////
  1129. public String toCode()
  1130. {
  1131. StringBuilder s = new StringBuilder(str.length()+4);
  1132. s.append('`');
  1133. int len = str.length();
  1134. for (int i=0; i<len; ++i)
  1135. {
  1136. int c = str.charAt(i);
  1137. switch (c)
  1138. {
  1139. case '\n': s.append('\\').append('n'); break;
  1140. case '\r': s.append('\\').append('r'); break;
  1141. case '\f': s.append('\\').append('f'); break;
  1142. case '\t': s.append('\\').append('t'); break;
  1143. case '`': s.append('\\').append('`'); break;
  1144. case '$': s.append('\\').append('$'); break;
  1145. default: s.append((char)c);
  1146. }
  1147. }
  1148. // closing quote
  1149. return s.append('`').toString();
  1150. }
  1151. //////////////////////////////////////////////////////////////////////////
  1152. // Utils
  1153. //////////////////////////////////////////////////////////////////////////
  1154. public static boolean isName(String name)
  1155. {
  1156. int len = name.length();
  1157. // must be at least one character long
  1158. if (len == 0) return false;
  1159. // check for "." and ".."
  1160. if (name.charAt(0) == '.' && len <= 2)
  1161. {
  1162. if (len == 1) return false;
  1163. if (name.charAt(1) == '.') return false;
  1164. }
  1165. // check that each char is unreserved
  1166. for (int i=0; i<len; ++i)
  1167. {
  1168. int c = name.charAt(i);
  1169. if (c < 128 && nameMap[c]) continue;
  1170. return false;
  1171. }
  1172. return true;
  1173. }
  1174. public static void checkName(String name)
  1175. {
  1176. if (!isName(name))
  1177. throw NameErr.make(name);
  1178. }
  1179. static boolean isUpper(int c)
  1180. {
  1181. return 'A' <= c && c <= 'Z';
  1182. }
  1183. static int hexNibble(int ch)
  1184. {
  1185. if ((charMap[ch] & HEX) == 0) throw err("Invalid percent encoded hex: '" + (char)ch);
  1186. if (ch <= '9') return ch - '0';
  1187. if (ch <= 'Z') return (ch - 'A') + 10;
  1188. return (ch - 'a') + 10;
  1189. }
  1190. static RuntimeException err(String msg)
  1191. {
  1192. return ParseErr.make(msg);
  1193. }
  1194. //////////////////////////////////////////////////////////////////////////
  1195. // Character Map
  1196. //////////////////////////////////////////////////////////////////////////
  1197. static String toSection(int section)
  1198. {
  1199. switch (section)
  1200. {
  1201. case SCHEME: return "scheme";
  1202. case USER: return "userInfo";
  1203. case HOST: return "host";
  1204. case PATH: return "path";
  1205. case QUERY: return "query";
  1206. case FRAG: return "frag";
  1207. default: return "uri";
  1208. }
  1209. }
  1210. static boolean isScheme(int c) { return c < 128 ? (charMap[c] & SCHEME) != 0 : false; }
  1211. static final byte[] charMap = new byte[128];
  1212. static final boolean[] nameMap = new boolean[128];
  1213. static final byte[] delimEscMap = new byte[128];
  1214. static final int SCHEME = 0x01;
  1215. static final int USER = 0x02;
  1216. static final int HOST = 0x04;
  1217. static final int PATH = 0x08;
  1218. static final int QUERY = 0x10;
  1219. static final int FRAG = 0x20;
  1220. static final int DIGIT = 0x40;
  1221. static final int HEX = 0x80;
  1222. static
  1223. {
  1224. // alpha/digits characters
  1225. byte unreserved = SCHEME | USER | HOST | PATH | QUERY | FRAG;
  1226. for (int i='a'; i<='z'; ++i) { charMap[i] = unreserved; nameMap[i] = true; }
  1227. for (int i='A'; i<='Z'; ++i) { charMap[i] = unreserved; nameMap[i] = true; }
  1228. for (int i='0'; i<='9'; ++i) { charMap[i] = unreserved; nameMap[i] = true; }
  1229. // unreserved symbols
  1230. charMap['-'] = unreserved; nameMap['-'] = true;
  1231. charMap['.'] = unreserved; nameMap['.'] = true;
  1232. charMap['_'] = unreserved; nameMap['_'] = true;
  1233. charMap['~'] = unreserved; nameMap['~'] = true;
  1234. // hex
  1235. for (int i='0'; i<='9'; ++i) charMap[i] |= HEX | DIGIT;
  1236. for (int i='a'; i<='f'; ++i) charMap[i] |= HEX;
  1237. for (int i='A'; i<='F'; ++i) charMap[i] |= HEX;
  1238. // sub-delimiter symbols
  1239. charMap['!'] = USER | HOST | PATH | QUERY | FRAG;
  1240. charMap['$'] = USER | HOST | PATH | QUERY | FRAG;
  1241. charMap['&'] = USER | HOST | PATH | QUERY | FRAG;
  1242. charMap['\''] = USER | HOST | PATH | QUERY | FRAG;
  1243. charMap['('] = USER | HOST | PATH | QUERY | FRAG;
  1244. charMap[')'] = USER | HOST | PATH | QUERY | FRAG;
  1245. charMap['*'] = USER | HOST | PATH | QUERY | FRAG;
  1246. charMap['+'] = SCHEME | USER | HOST | PATH | FRAG;
  1247. charMap[','] = USER | HOST | PATH | QUERY | FRAG;
  1248. charMap[';'] = USER | HOST | PATH | QUERY | FRAG;
  1249. charMap['='] = USER | HOST | PATH | QUERY | FRAG;
  1250. // gen-delimiter symbols
  1251. charMap[':'] = PATH | USER | QUERY | FRAG;
  1252. charMap['/'] = PATH | QUERY | FRAG;
  1253. charMap['?'] = QUERY | FRAG;
  1254. charMap['#'] = 0;
  1255. charMap['['] = 0;
  1256. charMap[']'] = 0;
  1257. charMap['@'] = PATH | QUERY | FRAG;
  1258. // delimiter escape map - which characters need to
  1259. // be backslashed escaped in each section
  1260. delimEscMap[':'] = PATH;
  1261. delimEscMap['/'] = PATH;
  1262. delimEscMap['?'] = PATH;
  1263. delimEscMap['#'] = PATH | QUERY;
  1264. delimEscMap['&'] = QUERY;
  1265. delimEscMap[';'] = QUERY;
  1266. delimEscMap['='] = QUERY;
  1267. delimEscMap['\\'] = SCHEME | USER | HOST | PATH | QUERY | FRAG;
  1268. }
  1269. //////////////////////////////////////////////////////////////////////////
  1270. // Empty Path/Query
  1271. //////////////////////////////////////////////////////////////////////////
  1272. static List emptyPath() { return Sys.StrType.emptyList(); }
  1273. static Map emptyQuery() { return Sys.emptyStrStrMap; }
  1274. //////////////////////////////////////////////////////////////////////////
  1275. // Fields
  1276. //////////////////////////////////////////////////////////////////////////
  1277. static final Range parentRange = Range.make(0L, -2L, false);
  1278. public static final Uri defVal = fromStr("");
  1279. final String str;
  1280. final String scheme;
  1281. final String userInfo;
  1282. final String host;
  1283. final Long port;
  1284. final List path;
  1285. final String pathStr;
  1286. final Map query;
  1287. final String queryStr;
  1288. final String frag;
  1289. String encoded;
  1290. }