001: /*
002: * $Header: /home/cvs/jakarta-tomcat-4.0/catalina/src/share/org/apache/catalina/util/URL.java,v 1.4 2002/06/16 02:54:03 billbarker Exp $
003: * $Revision: 1.4 $
004: * $Date: 2002/06/16 02:54:03 $
005: *
006: * ====================================================================
007: *
008: * The Apache Software License, Version 1.1
009: *
010: * Copyright (c) 1999 The Apache Software Foundation. All rights
011: * reserved.
012: *
013: * Redistribution and use in source and binary forms, with or without
014: * modification, are permitted provided that the following conditions
015: * are met:
016: *
017: * 1. Redistributions of source code must retain the above copyright
018: * notice, this list of conditions and the following disclaimer.
019: *
020: * 2. Redistributions in binary form must reproduce the above copyright
021: * notice, this list of conditions and the following disclaimer in
022: * the documentation and/or other materials provided with the
023: * distribution.
024: *
025: * 3. The end-user documentation included with the redistribution, if
026: * any, must include the following acknowlegement:
027: * "This product includes software developed by the
028: * Apache Software Foundation (http://www.apache.org/)."
029: * Alternately, this acknowlegement may appear in the software itself,
030: * if and wherever such third-party acknowlegements normally appear.
031: *
032: * 4. The names "The Jakarta Project", "Tomcat", and "Apache Software
033: * Foundation" must not be used to endorse or promote products derived
034: * from this software without prior written permission. For written
035: * permission, please contact apache@apache.org.
036: *
037: * 5. Products derived from this software may not be called "Apache"
038: * nor may "Apache" appear in their names without prior written
039: * permission of the Apache Group.
040: *
041: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
042: * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
043: * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
044: * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
045: * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
046: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
047: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
048: * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
049: * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
050: * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
051: * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
052: * SUCH DAMAGE.
053: * ====================================================================
054: *
055: * This software consists of voluntary contributions made by many
056: * individuals on behalf of the Apache Software Foundation. For more
057: * information on the Apache Software Foundation, please see
058: * <http://www.apache.org/>.
059: *
060: * [Additional notices, if required by prior licensing conditions]
061: *
062: */
063:
064: package org.apache.catalina.util;
065:
066: import java.io.Serializable;
067: import java.net.MalformedURLException;
068:
069: /**
070: * <p><strong>URL</strong> is designed to provide public APIs for parsing
071: * and synthesizing Uniform Resource Locators as similar as possible to the
072: * APIs of <code>java.net.URL</code>, but without the ability to open a
073: * stream or connection. One of the consequences of this is that you can
074: * construct URLs for protocols for which a URLStreamHandler is not
075: * available (such as an "https" URL when JSSE is not installed).</p>
076: *
077: * <p><strong>WARNING</strong> - This class assumes that the string
078: * representation of a URL conforms to the <code>spec</code> argument
079: * as described in RFC 2396 "Uniform Resource Identifiers: Generic Syntax":
080: * <pre>
081: * <scheme>//<authority><path>?<query>#<fragment>
082: * </pre></p>
083: *
084: * <p><strong>FIXME</strong> - This class really ought to end up in a Commons
085: * package someplace.</p>
086: *
087: * @author Craig R. McClanahan
088: * @version $Revision: 1.4 $ $Date: 2002/06/16 02:54:03 $
089: */
090:
091: public final class URL implements Serializable {
092:
093: // ----------------------------------------------------------- Constructors
094:
095: /**
096: * Create a URL object from the specified String representation.
097: *
098: * @param spec String representation of the URL
099: *
100: * @exception MalformedURLException if the string representation
101: * cannot be parsed successfully
102: */
103: public URL(String spec) throws MalformedURLException {
104:
105: this (null, spec);
106:
107: }
108:
109: /**
110: * Create a URL object by parsing a string representation relative
111: * to a specified context. Based on logic from JDK 1.3.1's
112: * <code>java.net.URL</code>.
113: *
114: * @param context URL against which the relative representation
115: * is resolved
116: * @param spec String representation of the URL (usually relative)
117: *
118: * @exception MalformedURLException if the string representation
119: * cannot be parsed successfully
120: */
121: public URL(URL context, String spec) throws MalformedURLException {
122:
123: String original = spec;
124: int i, limit, c;
125: int start = 0;
126: String newProtocol = null;
127: boolean aRef = false;
128:
129: try {
130:
131: // Eliminate leading and trailing whitespace
132: limit = spec.length();
133: while ((limit > 0) && (spec.charAt(limit - 1) <= ' ')) {
134: limit--;
135: }
136: while ((start < limit) && (spec.charAt(start) <= ' ')) {
137: start++;
138: }
139:
140: // If the string representation starts with "url:", skip it
141: if (spec.regionMatches(true, start, "url:", 0, 4)) {
142: start += 4;
143: }
144:
145: // Is this a ref relative to the context URL?
146: if ((start < spec.length()) && (spec.charAt(start) == '#')) {
147: aRef = true;
148: }
149:
150: // Parse out the new protocol
151: for (i = start; !aRef && (i < limit)
152: && ((c = spec.charAt(i)) != '/'); i++) {
153: if (c == ':') {
154: String s = spec.substring(start, i).toLowerCase();
155: // Assume all protocols are valid
156: newProtocol = s;
157: start = i + 1;
158: break;
159: }
160: }
161:
162: // Only use our context if the protocols match
163: protocol = newProtocol;
164: if ((context != null)
165: && ((newProtocol == null) || newProtocol
166: .equalsIgnoreCase(context.getProtocol()))) {
167: // If the context is a hierarchical URL scheme and the spec
168: // contains a matching scheme then maintain backwards
169: // compatibility and treat it as if the spec didn't contain
170: // the scheme; see 5.2.3 of RFC2396
171: if ((context.getPath() != null)
172: && (context.getPath().startsWith("/")))
173: newProtocol = null;
174: if (newProtocol == null) {
175: protocol = context.getProtocol();
176: authority = context.getAuthority();
177: userInfo = context.getUserInfo();
178: host = context.getHost();
179: port = context.getPort();
180: file = context.getFile();
181: int question = file.lastIndexOf("?");
182: if (question < 0)
183: path = file;
184: else
185: path = file.substring(0, question);
186: }
187: }
188:
189: if (protocol == null)
190: throw new MalformedURLException("no protocol: "
191: + original);
192:
193: // Parse out any ref portion of the spec
194: i = spec.indexOf('#', start);
195: if (i >= 0) {
196: ref = spec.substring(i + 1, limit);
197: limit = i;
198: }
199:
200: // Parse the remainder of the spec in a protocol-specific fashion
201: parse(spec, start, limit);
202: if (context != null)
203: normalize();
204:
205: } catch (MalformedURLException e) {
206: throw e;
207: } catch (Exception e) {
208: throw new MalformedURLException(e.toString());
209: }
210:
211: }
212:
213: /**
214: * Create a URL object from the specified components. The default port
215: * number for the specified protocol will be used.
216: *
217: * @param protocol Name of the protocol to use
218: * @param host Name of the host addressed by this protocol
219: * @param file Filename on the specified host
220: *
221: * @exception MalformedURLException is never thrown, but present for
222: * compatible APIs
223: */
224: public URL(String protocol, String host, String file)
225: throws MalformedURLException {
226:
227: this (protocol, host, -1, file);
228:
229: }
230:
231: /**
232: * Create a URL object from the specified components. Specifying a port
233: * number of -1 indicates that the URL should use the default port for
234: * that protocol. Based on logic from JDK 1.3.1's
235: * <code>java.net.URL</code>.
236: *
237: * @param protocol Name of the protocol to use
238: * @param host Name of the host addressed by this protocol
239: * @param port Port number, or -1 for the default port for this protocol
240: * @param file Filename on the specified host
241: *
242: * @exception MalformedURLException is never thrown, but present for
243: * compatible APIs
244: */
245: public URL(String protocol, String host, int port, String file)
246: throws MalformedURLException {
247:
248: this .protocol = protocol;
249: this .host = host;
250: this .port = port;
251:
252: int hash = file.indexOf('#');
253: this .file = hash < 0 ? file : file.substring(0, hash);
254: this .ref = hash < 0 ? null : file.substring(hash + 1);
255: int question = file.lastIndexOf('?');
256: if (question >= 0) {
257: query = file.substring(question + 1);
258: path = file.substring(0, question);
259: } else
260: path = file;
261:
262: if ((host != null) && (host.length() > 0))
263: authority = (port == -1) ? host : host + ":" + port;
264:
265: }
266:
267: // ----------------------------------------------------- Instance Variables
268:
269: /**
270: * The authority part of the URL.
271: */
272: private String authority = null;
273:
274: /**
275: * The filename part of the URL.
276: */
277: private String file = null;
278:
279: /**
280: * The host name part of the URL.
281: */
282: private String host = null;
283:
284: /**
285: * The path part of the URL.
286: */
287: private String path = null;
288:
289: /**
290: * The port number part of the URL.
291: */
292: private int port = -1;
293:
294: /**
295: * The protocol name part of the URL.
296: */
297: private String protocol = null;
298:
299: /**
300: * The query part of the URL.
301: */
302: private String query = null;
303:
304: /**
305: * The reference part of the URL.
306: */
307: private String ref = null;
308:
309: /**
310: * The user info part of the URL.
311: */
312: private String userInfo = null;
313:
314: // --------------------------------------------------------- Public Methods
315:
316: /**
317: * Compare two URLs for equality. The result is <code>true</code> if and
318: * only if the argument is not null, and is a <code>URL</code> object
319: * that represents the same <code>URL</code> as this object. Two
320: * <code>URLs</code> are equal if they have the same protocol and
321: * reference the same host, the same port number on the host,
322: * and the same file and anchor on the host.
323: *
324: * @param obj The URL to compare against
325: */
326: public boolean equals(Object obj) {
327:
328: if (obj == null)
329: return (false);
330: if (!(obj instanceof URL))
331: return (false);
332: URL other = (URL) obj;
333: if (!sameFile(other))
334: return (false);
335: return (compare(ref, other.getRef()));
336:
337: }
338:
339: /**
340: * Return the authority part of the URL.
341: */
342: public String getAuthority() {
343:
344: return (this .authority);
345:
346: }
347:
348: /**
349: * Return the filename part of the URL. <strong>NOTE</strong> - For
350: * compatibility with <code>java.net.URL</code>, this value includes
351: * the query string if there was one. For just the path portion,
352: * call <code>getPath()</code> instead.
353: */
354: public String getFile() {
355:
356: if (file == null)
357: return ("");
358: return (this .file);
359:
360: }
361:
362: /**
363: * Return the host name part of the URL.
364: */
365: public String getHost() {
366:
367: return (this .host);
368:
369: }
370:
371: /**
372: * Return the path part of the URL.
373: */
374: public String getPath() {
375:
376: if (this .path == null)
377: return ("");
378: return (this .path);
379:
380: }
381:
382: /**
383: * Return the port number part of the URL.
384: */
385: public int getPort() {
386:
387: return (this .port);
388:
389: }
390:
391: /**
392: * Return the protocol name part of the URL.
393: */
394: public String getProtocol() {
395:
396: return (this .protocol);
397:
398: }
399:
400: /**
401: * Return the query part of the URL.
402: */
403: public String getQuery() {
404:
405: return (this .query);
406:
407: }
408:
409: /**
410: * Return the reference part of the URL.
411: */
412: public String getRef() {
413:
414: return (this .ref);
415:
416: }
417:
418: /**
419: * Return the user info part of the URL.
420: */
421: public String getUserInfo() {
422:
423: return (this .userInfo);
424:
425: }
426:
427: /**
428: * Normalize the <code>path</code> (and therefore <code>file</code>)
429: * portions of this URL.
430: * <p>
431: * <strong>NOTE</strong> - This method is not part of the public API
432: * of <code>java.net.URL</code>, but is provided as a value added
433: * service of this implementation.
434: *
435: * @exception MalformedURLException if a normalization error occurs,
436: * such as trying to move about the hierarchical root
437: */
438: public void normalize() throws MalformedURLException {
439:
440: // Special case for null path
441: if (path == null) {
442: if (query != null)
443: file = "?" + query;
444: else
445: file = "";
446: return;
447: }
448:
449: // Create a place for the normalized path
450: String normalized = path;
451: if (normalized.equals("/.")) {
452: path = "/";
453: if (query != null)
454: file = path + "?" + query;
455: else
456: file = path;
457: return;
458: }
459:
460: // Normalize the slashes and add leading slash if necessary
461: if (normalized.indexOf('\\') >= 0)
462: normalized = normalized.replace('\\', '/');
463: if (!normalized.startsWith("/"))
464: normalized = "/" + normalized;
465:
466: // Resolve occurrences of "//" in the normalized path
467: while (true) {
468: int index = normalized.indexOf("//");
469: if (index < 0)
470: break;
471: normalized = normalized.substring(0, index)
472: + normalized.substring(index + 1);
473: }
474:
475: // Resolve occurrences of "/./" in the normalized path
476: while (true) {
477: int index = normalized.indexOf("/./");
478: if (index < 0)
479: break;
480: normalized = normalized.substring(0, index)
481: + normalized.substring(index + 2);
482: }
483:
484: // Resolve occurrences of "/../" in the normalized path
485: while (true) {
486: int index = normalized.indexOf("/../");
487: if (index < 0)
488: break;
489: if (index == 0)
490: throw new MalformedURLException(
491: "Invalid relative URL reference");
492: int index2 = normalized.lastIndexOf('/', index - 1);
493: normalized = normalized.substring(0, index2)
494: + normalized.substring(index + 3);
495: }
496:
497: // Resolve occurrences of "/." at the end of the normalized path
498: if (normalized.endsWith("/."))
499: normalized = normalized.substring(0,
500: normalized.length() - 1);
501:
502: // Resolve occurrences of "/.." at the end of the normalized path
503: if (normalized.endsWith("/..")) {
504: int index = normalized.length() - 3;
505: int index2 = normalized.lastIndexOf('/', index - 1);
506: if (index2 < 0)
507: throw new MalformedURLException(
508: "Invalid relative URL reference");
509: normalized = normalized.substring(0, index2 + 1);
510: }
511:
512: // Return the normalized path that we have completed
513: path = normalized;
514: if (query != null)
515: file = path + "?" + query;
516: else
517: file = path;
518:
519: }
520:
521: /**
522: * Compare two URLs, excluding the "ref" fields. Returns <code>true</code>
523: * if this <code>URL</code> and the <code>other</code> argument both refer
524: * to the same resource. The two <code>URLs</code> might not both contain
525: * the same anchor.
526: */
527: public boolean sameFile(URL other) {
528:
529: if (!compare(protocol, other.getProtocol()))
530: return (false);
531: if (!compare(host, other.getHost()))
532: return (false);
533: if (port != other.getPort())
534: return (false);
535: if (!compare(file, other.getFile()))
536: return (false);
537: return (true);
538:
539: }
540:
541: /**
542: * Return a string representation of this URL. This follow the rules in
543: * RFC 2396, Section 5.2, Step 7.
544: */
545: public String toExternalForm() {
546:
547: StringBuffer sb = new StringBuffer();
548: if (protocol != null) {
549: sb.append(protocol);
550: sb.append(":");
551: }
552: if (authority != null) {
553: sb.append("//");
554: sb.append(authority);
555: }
556: if (path != null)
557: sb.append(path);
558: if (query != null) {
559: sb.append('?');
560: sb.append(query);
561: }
562: if (ref != null) {
563: sb.append('#');
564: sb.append(ref);
565: }
566: return (sb.toString());
567:
568: }
569:
570: /**
571: * Return a string representation of this object.
572: */
573: public String toString() {
574:
575: StringBuffer sb = new StringBuffer("URL[");
576: sb.append("authority=");
577: sb.append(authority);
578: sb.append(", file=");
579: sb.append(file);
580: sb.append(", host=");
581: sb.append(host);
582: sb.append(", port=");
583: sb.append(port);
584: sb.append(", protocol=");
585: sb.append(protocol);
586: sb.append(", query=");
587: sb.append(query);
588: sb.append(", ref=");
589: sb.append(ref);
590: sb.append(", userInfo=");
591: sb.append(userInfo);
592: sb.append("]");
593: return (sb.toString());
594:
595: // return (toExternalForm());
596:
597: }
598:
599: // -------------------------------------------------------- Private Methods
600:
601: /**
602: * Compare to String values for equality, taking appropriate care if one
603: * or both of the values are <code>null</code>.
604: *
605: * @param first First string
606: * @param second Second string
607: */
608: private boolean compare(String first, String second) {
609:
610: if (first == null) {
611: if (second == null)
612: return (true);
613: else
614: return (false);
615: } else {
616: if (second == null)
617: return (false);
618: else
619: return (first.equals(second));
620: }
621:
622: }
623:
624: /**
625: * Parse the specified portion of the string representation of a URL,
626: * assuming that it has a format similar to that for <code>http</code>.
627: *
628: * <p><strong>FIXME</strong> - This algorithm can undoubtedly be optimized
629: * for performance. However, that needs to wait until after sufficient
630: * unit tests are implemented to guarantee correct behavior with no
631: * regressions.</p>
632: *
633: * @param spec String representation being parsed
634: * @param start Starting offset, which will be just after the ':' (if
635: * there is one) that determined the protocol name
636: * @param limit Ending position, which will be the position of the '#'
637: * (if there is one) that delimited the anchor
638: *
639: * @exception MalformedURLException if a parsing error occurs
640: */
641: private void parse(String spec, int start, int limit)
642: throws MalformedURLException {
643:
644: // Trim the query string (if any) off the tail end
645: int question = spec.lastIndexOf('?', limit - 1);
646: if ((question >= 0) && (question < limit)) {
647: query = spec.substring(question + 1, limit);
648: limit = question;
649: } else {
650: query = null;
651: }
652:
653: // Parse the authority section
654: if (spec.indexOf("//", start) == start) {
655: int pathStart = spec.indexOf("/", start + 2);
656: if ((pathStart >= 0) && (pathStart < limit)) {
657: authority = spec.substring(start + 2, pathStart);
658: start = pathStart;
659: } else {
660: authority = spec.substring(start + 2, limit);
661: start = limit;
662: }
663: if (authority.length() > 0) {
664: int at = authority.indexOf('@');
665: if (at >= 0) {
666: userInfo = authority.substring(0, at);
667: }
668: int colon = authority.indexOf(':', at + 1);
669: if (colon >= 0) {
670: try {
671: port = Integer.parseInt(authority
672: .substring(colon + 1));
673: } catch (NumberFormatException e) {
674: throw new MalformedURLException(e.toString());
675: }
676: host = authority.substring(at + 1, colon);
677: } else {
678: host = authority.substring(at + 1);
679: port = -1;
680: }
681: }
682: }
683:
684: // Parse the path section
685: if (spec.indexOf("/", start) == start) { // Absolute path
686: path = spec.substring(start, limit);
687: if (query != null)
688: file = path + "?" + query;
689: else
690: file = path;
691: return;
692: }
693:
694: // Resolve relative path against our context's file
695: if (path == null) {
696: if (query != null)
697: file = "?" + query;
698: else
699: file = null;
700: return;
701: }
702: if (!path.startsWith("/"))
703: throw new MalformedURLException(
704: "Base path does not start with '/'");
705: if (!path.endsWith("/"))
706: path += "/../";
707: path += spec.substring(start, limit);
708: if (query != null)
709: file = path + "?" + query;
710: else
711: file = path;
712: return;
713:
714: }
715:
716: }
|