1/*
2 *  Licensed to the Apache Software Foundation (ASF) under one or more
3 *  contributor license agreements.  See the NOTICE file distributed with
4 *  this work for additional information regarding copyright ownership.
5 *  The ASF licenses this file to You under the Apache License, Version 2.0
6 *  (the "License"); you may not use this file except in compliance with
7 *  the License.  You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 *  Unless required by applicable law or agreed to in writing, software
12 *  distributed under the License is distributed on an "AS IS" BASIS,
13 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *  See the License for the specific language governing permissions and
15 *  limitations under the License.
16 */
17
18package java.net;
19
20import java.io.IOException;
21import java.io.InputStream;
22import java.io.ObjectInputStream;
23import java.io.ObjectOutputStream;
24import java.io.Serializable;
25import java.util.Hashtable;
26import java.util.jar.JarFile;
27import libcore.net.url.FileHandler;
28import libcore.net.url.FtpHandler;
29import libcore.net.url.JarHandler;
30import libcore.net.url.UrlUtils;
31
32/**
33 * A Uniform Resource Locator that identifies the location of an Internet
34 * resource as specified by <a href="http://www.ietf.org/rfc/rfc1738.txt">RFC
35 * 1738</a>.
36 *
37 * <h3>Parts of a URL</h3>
38 * A URL is composed of many parts. This class can both parse URL strings into
39 * parts and compose URL strings from parts. For example, consider the parts of
40 * this URL:
41 * {@code http://username:password@host:8080/directory/file?query#ref}:
42 * <table>
43 * <tr><th>Component</th><th>Example value</th><th>Also known as</th></tr>
44 * <tr><td>{@link #getProtocol() Protocol}</td><td>{@code http}</td><td>scheme</td></tr>
45 * <tr><td>{@link #getAuthority() Authority}</td><td>{@code username:password@host:8080}</td><td></td></tr>
46 * <tr><td>{@link #getUserInfo() User Info}</td><td>{@code username:password}</td><td></td></tr>
47 * <tr><td>{@link #getHost() Host}</td><td>{@code host}</td><td></td></tr>
48 * <tr><td>{@link #getPort() Port}</td><td>{@code 8080}</td><td></td></tr>
49 * <tr><td>{@link #getFile() File}</td><td>{@code /directory/file?query}</td><td></td></tr>
50 * <tr><td>{@link #getPath() Path}</td><td>{@code /directory/file}</td><td></td></tr>
51 * <tr><td>{@link #getQuery() Query}</td><td>{@code query}</td><td></td></tr>
52 * <tr><td>{@link #getRef() Ref}</td><td>{@code ref}</td><td>fragment</td></tr>
53 * </table>
54 *
55 * <h3>Supported Protocols</h3>
56 * This class may be used to construct URLs with the following protocols:
57 * <ul>
58 * <li><strong>file</strong>: read files from the local filesystem.
59 * <li><strong>ftp</strong>: <a href="http://www.ietf.org/rfc/rfc959.txt">File
60 *     Transfer Protocol</a>
61 * <li><strong>http</strong>: <a href="http://www.ietf.org/rfc/rfc2616.txt">Hypertext
62 *     Transfer Protocol</a>
63 * <li><strong>https</strong>: <a href="http://www.ietf.org/rfc/rfc2818.txt">HTTP
64 *     over TLS</a>
65 * <li><strong>jar</strong>: read {@link JarFile Jar files} from the
66 *     filesystem</li>
67 * </ul>
68 * In general, attempts to create URLs with any other protocol will fail with a
69 * {@link MalformedURLException}. Applications may install handlers for other
70 * schemes using {@link #setURLStreamHandlerFactory} or with the {@code
71 * java.protocol.handler.pkgs} system property.
72 *
73 * <p>The {@link URI} class can be used to manipulate URLs of any protocol.
74 */
75public final class URL implements Serializable {
76    private static final long serialVersionUID = -7627629688361524110L;
77
78    private static URLStreamHandlerFactory streamHandlerFactory;
79
80    /** Cache of protocols to their handlers */
81    private static final Hashtable<String, URLStreamHandler> streamHandlers
82            = new Hashtable<String, URLStreamHandler>();
83
84    private String protocol;
85    private String authority;
86    private String host;
87    private int port = -1;
88    private String file;
89    private String ref;
90
91    private transient String userInfo;
92    private transient String path;
93    private transient String query;
94
95    transient URLStreamHandler streamHandler;
96
97    /**
98     * The cached hash code, or 0 if it hasn't been computed yet. Unlike the RI,
99     * this implementation's hashCode is transient because the hash code is
100     * unspecified and may vary between VMs or versions.
101     */
102    private transient int hashCode;
103
104    /**
105     * Sets the stream handler factory for this VM.
106     *
107     * @throws Error if a URLStreamHandlerFactory has already been installed
108     *     for the current VM.
109     */
110    public static synchronized void setURLStreamHandlerFactory(URLStreamHandlerFactory factory) {
111        if (streamHandlerFactory != null) {
112            throw new Error("Factory already set");
113        }
114        streamHandlers.clear();
115        streamHandlerFactory = factory;
116    }
117
118    /**
119     * Creates a new URL instance by parsing {@code spec}.
120     *
121     * @throws MalformedURLException if {@code spec} could not be parsed as a
122     *     URL.
123     */
124    public URL(String spec) throws MalformedURLException {
125        this((URL) null, spec, null);
126    }
127
128    /**
129     * Creates a new URL by resolving {@code spec} relative to {@code context}.
130     *
131     * @param context the URL to which {@code spec} is relative, or null for
132     *     no context in which case {@code spec} must be an absolute URL.
133     * @throws MalformedURLException if {@code spec} could not be parsed as a
134     *     URL or has an unsupported protocol.
135     */
136    public URL(URL context, String spec) throws MalformedURLException {
137        this(context, spec, null);
138    }
139
140    /**
141     * Creates a new URL by resolving {@code spec} relative to {@code context}.
142     *
143     * @param context the URL to which {@code spec} is relative, or null for
144     *     no context in which case {@code spec} must be an absolute URL.
145     * @param handler the stream handler for this URL, or null for the
146     *     protocol's default stream handler.
147     * @throws MalformedURLException if the given string {@code spec} could not
148     *     be parsed as a URL or an invalid protocol has been found.
149     */
150    public URL(URL context, String spec, URLStreamHandler handler) throws MalformedURLException {
151        if (spec == null) {
152            throw new MalformedURLException();
153        }
154        if (handler != null) {
155            streamHandler = handler;
156        }
157        spec = spec.trim();
158
159        protocol = UrlUtils.getSchemePrefix(spec);
160        int schemeSpecificPartStart = protocol != null ? (protocol.length() + 1) : 0;
161
162        // If the context URL has a different protocol, discard it because we can't use it.
163        if (protocol != null && context != null && !protocol.equals(context.protocol)) {
164            context = null;
165        }
166
167        // Inherit from the context URL if it exists.
168        if (context != null) {
169            set(context.protocol, context.getHost(), context.getPort(), context.getAuthority(),
170                    context.getUserInfo(), context.getPath(), context.getQuery(),
171                    context.getRef());
172            if (streamHandler == null) {
173                streamHandler = context.streamHandler;
174            }
175        } else if (protocol == null) {
176            throw new MalformedURLException("Protocol not found: " + spec);
177        }
178
179        if (streamHandler == null) {
180            setupStreamHandler();
181            if (streamHandler == null) {
182                throw new MalformedURLException("Unknown protocol: " + protocol);
183            }
184        }
185
186        // Parse the URL. If the handler throws any exception, throw MalformedURLException instead.
187        try {
188            streamHandler.parseURL(this, spec, schemeSpecificPartStart, spec.length());
189        } catch (Exception e) {
190            throw new MalformedURLException(e.toString());
191        }
192    }
193
194    /**
195     * Creates a new URL of the given component parts. The URL uses the
196     * protocol's default port.
197     *
198     * @throws MalformedURLException if the combination of all arguments do not
199     *     represent a valid URL or if the protocol is invalid.
200     */
201    public URL(String protocol, String host, String file) throws MalformedURLException {
202        this(protocol, host, -1, file, null);
203    }
204
205    /**
206     * Creates a new URL of the given component parts. The URL uses the
207     * protocol's default port.
208     *
209     * @param host the host name or IP address of the new URL.
210     * @param port the port, or {@code -1} for the protocol's default port.
211     * @param file the name of the resource.
212     * @throws MalformedURLException if the combination of all arguments do not
213     *     represent a valid URL or if the protocol is invalid.
214     */
215    public URL(String protocol, String host, int port, String file) throws MalformedURLException {
216        this(protocol, host, port, file, null);
217    }
218
219    /**
220     * Creates a new URL of the given component parts. The URL uses the
221     * protocol's default port.
222     *
223     * @param host the host name or IP address of the new URL.
224     * @param port the port, or {@code -1} for the protocol's default port.
225     * @param file the name of the resource.
226     * @param handler the stream handler for this URL, or null for the
227     *     protocol's default stream handler.
228     * @throws MalformedURLException if the combination of all arguments do not
229     *     represent a valid URL or if the protocol is invalid.
230     */
231    public URL(String protocol, String host, int port, String file,
232            URLStreamHandler handler) throws MalformedURLException {
233        if (port < -1) {
234            throw new MalformedURLException("port < -1: " + port);
235        }
236        if (protocol == null) {
237            throw new NullPointerException("protocol == null");
238        }
239
240        // Wrap IPv6 addresses in square brackets if they aren't already.
241        if (host != null && host.contains(":") && host.charAt(0) != '[') {
242            host = "[" + host + "]";
243        }
244
245        this.protocol = protocol;
246        this.host = host;
247        this.port = port;
248
249        file = UrlUtils.authoritySafePath(host, file);
250
251        // Set the fields from the arguments. Handle the case where the
252        // passed in "file" includes both a file and a reference part.
253        int hash = file.indexOf("#");
254        if (hash != -1) {
255            this.file = file.substring(0, hash);
256            this.ref = file.substring(hash + 1);
257        } else {
258            this.file = file;
259        }
260        fixURL(false);
261
262        // Set the stream handler for the URL either to the handler
263        // argument if it was specified, or to the default for the
264        // receiver's protocol if the handler was null.
265        if (handler == null) {
266            setupStreamHandler();
267            if (streamHandler == null) {
268                throw new MalformedURLException("Unknown protocol: " + protocol);
269            }
270        } else {
271            streamHandler = handler;
272        }
273    }
274
275    void fixURL(boolean fixHost) {
276        int index;
277        if (host != null && host.length() > 0) {
278            authority = host;
279            if (port != -1) {
280                authority = authority + ":" + port;
281            }
282        }
283        if (fixHost) {
284            if (host != null && (index = host.lastIndexOf('@')) > -1) {
285                userInfo = host.substring(0, index);
286                host = host.substring(index + 1);
287            } else {
288                userInfo = null;
289            }
290        }
291        if (file != null && (index = file.indexOf('?')) > -1) {
292            query = file.substring(index + 1);
293            path = file.substring(0, index);
294        } else {
295            query = null;
296            path = file;
297        }
298    }
299
300    /**
301     * Sets the properties of this URL using the provided arguments. Only a
302     * {@code URLStreamHandler} can use this method to set fields of the
303     * existing URL instance. A URL is generally constant.
304     */
305    protected void set(String protocol, String host, int port, String file, String ref) {
306        if (this.protocol == null) {
307            this.protocol = protocol;
308        }
309        this.host = host;
310        this.file = file;
311        this.port = port;
312        this.ref = ref;
313        hashCode = 0;
314        fixURL(true);
315    }
316
317    /**
318     * Returns true if this URL equals {@code o}. URLs are equal if they have
319     * the same protocol, host, port, file, and reference.
320     *
321     * <h3>Network I/O Warning</h3>
322     * <p>Some implementations of URL.equals() resolve host names over the
323     * network. This is problematic:
324     * <ul>
325     * <li><strong>The network may be slow.</strong> Many classes, including
326     * core collections like {@link java.util.Map Map} and {@link java.util.Set
327     * Set} expect that {@code equals} and {@code hashCode} will return quickly.
328     * By violating this assumption, this method posed potential performance
329     * problems.
330     * <li><strong>Equal IP addresses do not imply equal content.</strong>
331     * Virtual hosting permits unrelated sites to share an IP address. This
332     * method could report two otherwise unrelated URLs to be equal because
333     * they're hosted on the same server.</li>
334     * <li><strong>The network may not be available.</strong> Two URLs could be
335     * equal when a network is available and unequal otherwise.</li>
336     * <li><strong>The network may change.</strong> The IP address for a given
337     * host name varies by network and over time. This is problematic for mobile
338     * devices. Two URLs could be equal on some networks and unequal on
339     * others.</li>
340     * </ul>
341     * <p>This problem is fixed in Android 4.0 (Ice Cream Sandwich). In that
342     * release, URLs are only equal if their host names are equal (ignoring
343     * case).
344     */
345    @Override public boolean equals(Object o) {
346        if (o == null) {
347            return false;
348        }
349        if (this == o) {
350            return true;
351        }
352        if (this.getClass() != o.getClass()) {
353            return false;
354        }
355        return streamHandler.equals(this, (URL) o);
356    }
357
358    /**
359     * Returns true if this URL refers to the same resource as {@code otherURL}.
360     * All URL components except the reference field are compared.
361     */
362    public boolean sameFile(URL otherURL) {
363        return streamHandler.sameFile(this, otherURL);
364    }
365
366    @Override public int hashCode() {
367        if (hashCode == 0) {
368            hashCode = streamHandler.hashCode(this);
369        }
370        return hashCode;
371    }
372
373    /**
374     * Sets the receiver's stream handler to one which is appropriate for its
375     * protocol.
376     *
377     * <p>Note that this will overwrite any existing stream handler with the new
378     * one. Senders must check if the streamHandler is null before calling the
379     * method if they do not want this behavior (a speed optimization).
380     *
381     * @throws MalformedURLException if no reasonable handler is available.
382     */
383    void setupStreamHandler() {
384        // Check for a cached (previously looked up) handler for
385        // the requested protocol.
386        streamHandler = streamHandlers.get(protocol);
387        if (streamHandler != null) {
388            return;
389        }
390
391        // If there is a stream handler factory, then attempt to
392        // use it to create the handler.
393        if (streamHandlerFactory != null) {
394            streamHandler = streamHandlerFactory.createURLStreamHandler(protocol);
395            if (streamHandler != null) {
396                streamHandlers.put(protocol, streamHandler);
397                return;
398            }
399        }
400
401        // Check if there is a list of packages which can provide handlers.
402        // If so, then walk this list looking for an applicable one.
403        String packageList = System.getProperty("java.protocol.handler.pkgs");
404        ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
405        if (packageList != null && contextClassLoader != null) {
406            for (String packageName : packageList.split("\\|")) {
407                String className = packageName + "." + protocol + ".Handler";
408                try {
409                    Class<?> c = contextClassLoader.loadClass(className);
410                    streamHandler = (URLStreamHandler) c.newInstance();
411                    if (streamHandler != null) {
412                        streamHandlers.put(protocol, streamHandler);
413                    }
414                    return;
415                } catch (IllegalAccessException ignored) {
416                } catch (InstantiationException ignored) {
417                } catch (ClassNotFoundException ignored) {
418                }
419            }
420        }
421
422        // Fall back to a built-in stream handler if the user didn't supply one
423        if (protocol.equals("file")) {
424            streamHandler = new FileHandler();
425        } else if (protocol.equals("ftp")) {
426            streamHandler = new FtpHandler();
427        } else if (protocol.equals("http")) {
428            try {
429                String name = "com.android.okhttp.HttpHandler";
430                streamHandler = (URLStreamHandler) Class.forName(name).newInstance();
431            } catch (Exception e) {
432                throw new AssertionError(e);
433            }
434        } else if (protocol.equals("https")) {
435            try {
436                String name = "com.android.okhttp.HttpsHandler";
437                streamHandler = (URLStreamHandler) Class.forName(name).newInstance();
438            } catch (Exception e) {
439                throw new AssertionError(e);
440            }
441        } else if (protocol.equals("jar")) {
442            streamHandler = new JarHandler();
443        }
444        if (streamHandler != null) {
445            streamHandlers.put(protocol, streamHandler);
446        }
447    }
448
449    /**
450     * Returns the content of the resource which is referred by this URL. By
451     * default this returns an {@code InputStream}, or null if the content type
452     * of the response is unknown.
453     */
454    public final Object getContent() throws IOException {
455        return openConnection().getContent();
456    }
457
458    /**
459     * Equivalent to {@code openConnection().getContent(types)}.
460     */
461    @SuppressWarnings("unchecked") // Param not generic in spec
462    public final Object getContent(Class[] types) throws IOException {
463        return openConnection().getContent(types);
464    }
465
466    /**
467     * Equivalent to {@code openConnection().getInputStream(types)}.
468     */
469    public final InputStream openStream() throws IOException {
470        return openConnection().getInputStream();
471    }
472
473    /**
474     * Returns a new connection to the resource referred to by this URL.
475     *
476     * @throws IOException if an error occurs while opening the connection.
477     */
478    public URLConnection openConnection() throws IOException {
479        return streamHandler.openConnection(this);
480    }
481
482    /**
483     * Returns a new connection to the resource referred to by this URL.
484     *
485     * @param proxy the proxy through which the connection will be established.
486     * @throws IOException if an I/O error occurs while opening the connection.
487     * @throws IllegalArgumentException if the argument proxy is null or of is
488     *     an invalid type.
489     * @throws UnsupportedOperationException if the protocol handler does not
490     *     support opening connections through proxies.
491     */
492    public URLConnection openConnection(Proxy proxy) throws IOException {
493        if (proxy == null) {
494            throw new IllegalArgumentException("proxy == null");
495        }
496        return streamHandler.openConnection(this, proxy);
497    }
498
499    /**
500     * Returns the URI equivalent to this URL.
501     *
502     * @throws URISyntaxException if this URL cannot be converted into a URI.
503     */
504    public URI toURI() throws URISyntaxException {
505        return new URI(toExternalForm());
506    }
507
508    /**
509     * Encodes this URL to the equivalent URI after escaping characters that are
510     * not permitted by URI.
511     *
512     * @hide
513     */
514    public URI toURILenient() throws URISyntaxException {
515        if (streamHandler == null) {
516            throw new IllegalStateException(protocol);
517        }
518        return new URI(streamHandler.toExternalForm(this, true));
519    }
520
521    /**
522     * Returns a string containing a concise, human-readable representation of
523     * this URL. The returned string is the same as the result of the method
524     * {@code toExternalForm()}.
525     */
526    @Override public String toString() {
527        return toExternalForm();
528    }
529
530    /**
531     * Returns a string containing a concise, human-readable representation of
532     * this URL.
533     */
534    public String toExternalForm() {
535        if (streamHandler == null) {
536            return "unknown protocol(" + protocol + ")://" + host + file;
537        }
538        return streamHandler.toExternalForm(this);
539    }
540
541    private void readObject(ObjectInputStream stream) throws IOException {
542        try {
543            stream.defaultReadObject();
544            if (host != null && authority == null) {
545                fixURL(true);
546            } else if (authority != null) {
547                int index;
548                if ((index = authority.lastIndexOf('@')) > -1) {
549                    userInfo = authority.substring(0, index);
550                }
551                if (file != null && (index = file.indexOf('?')) > -1) {
552                    query = file.substring(index + 1);
553                    path = file.substring(0, index);
554                } else {
555                    path = file;
556                }
557            }
558            setupStreamHandler();
559            if (streamHandler == null) {
560                throw new IOException("Unknown protocol: " + protocol);
561            }
562            hashCode = 0; // necessary until http://b/4471249 is fixed
563        } catch (ClassNotFoundException e) {
564            throw new IOException(e);
565        }
566    }
567
568    private void writeObject(ObjectOutputStream s) throws IOException {
569        s.defaultWriteObject();
570    }
571
572    /** @hide */
573    public int getEffectivePort() {
574        return URI.getEffectivePort(protocol, port);
575    }
576
577    /**
578     * Returns the protocol of this URL like "http" or "file". This is also
579     * known as the scheme. The returned string is lower case.
580     */
581    public String getProtocol() {
582        return protocol;
583    }
584
585    /**
586     * Returns the authority part of this URL, or null if this URL has no
587     * authority.
588     */
589    public String getAuthority() {
590        return authority;
591    }
592
593    /**
594     * Returns the user info of this URL, or null if this URL has no user info.
595     */
596    public String getUserInfo() {
597        return userInfo;
598    }
599
600    /**
601     * Returns the host name or IP address of this URL.
602     */
603    public String getHost() {
604        return host;
605    }
606
607    /**
608     * Returns the port number of this URL or {@code -1} if this URL has no
609     * explicit port.
610     *
611     * <p>If this URL has no explicit port, connections opened using this URL
612     * will use its {@link #getDefaultPort() default port}.
613     */
614    public int getPort() {
615        return port;
616    }
617
618    /**
619     * Returns the default port number of the protocol used by this URL. If no
620     * default port is defined by the protocol or the {@code URLStreamHandler},
621     * {@code -1} will be returned.
622     *
623     * @see URLStreamHandler#getDefaultPort
624     */
625    public int getDefaultPort() {
626        return streamHandler.getDefaultPort();
627    }
628
629    /**
630     * Returns the file of this URL.
631     */
632    public String getFile() {
633        return file;
634    }
635
636    /**
637     * Returns the path part of this URL.
638     */
639    public String getPath() {
640        return path;
641    }
642
643    /**
644     * Returns the query part of this URL, or null if this URL has no query.
645     */
646    public String getQuery() {
647        return query;
648    }
649
650    /**
651     * Returns the value of the reference part of this URL, or null if this URL
652     * has no reference part. This is also known as the fragment.
653     */
654    public String getRef() {
655        return ref;
656    }
657
658    /**
659     * Sets the properties of this URL using the provided arguments. Only a
660     * {@code URLStreamHandler} can use this method to set fields of the
661     * existing URL instance. A URL is generally constant.
662     */
663    protected void set(String protocol, String host, int port, String authority, String userInfo,
664            String path, String query, String ref) {
665        String file = path;
666        if (query != null && !query.isEmpty()) {
667            file += "?" + query;
668        }
669        set(protocol, host, port, file, ref);
670        this.authority = authority;
671        this.userInfo = userInfo;
672        this.path = path;
673        this.query = query;
674    }
675}
676