URLStreamHandler.java revision 2d99ef561304174b8ae01a0a68d5b96d5edb9f10
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 libcore.net.url.UrlUtils;
22import libcore.util.Objects;
23
24/**
25 * The abstract class {@code URLStreamHandler} is the base for all classes which
26 * can handle the communication with a URL object over a particular protocol
27 * type.
28 */
29public abstract class URLStreamHandler {
30    /**
31     * Establishes a new connection to the resource specified by the URL {@code
32     * u}. Since different protocols also have unique ways of connecting, it
33     * must be overwritten by the subclass.
34     *
35     * @param u
36     *            the URL to the resource where a connection has to be opened.
37     * @return the opened URLConnection to the specified resource.
38     * @throws IOException
39     *             if an I/O error occurs during opening the connection.
40     */
41    protected abstract URLConnection openConnection(URL u) throws IOException;
42
43    /**
44     * Establishes a new connection to the resource specified by the URL {@code
45     * u} using the given {@code proxy}. Since different protocols also have
46     * unique ways of connecting, it must be overwritten by the subclass.
47     *
48     * @param u
49     *            the URL to the resource where a connection has to be opened.
50     * @param proxy
51     *            the proxy that is used to make the connection.
52     * @return the opened URLConnection to the specified resource.
53     * @throws IOException
54     *             if an I/O error occurs during opening the connection.
55     * @throws IllegalArgumentException
56     *             if any argument is {@code null} or the type of proxy is
57     *             wrong.
58     * @throws UnsupportedOperationException
59     *             if the protocol handler doesn't support this method.
60     */
61    protected URLConnection openConnection(URL u, Proxy proxy) throws IOException {
62        throw new UnsupportedOperationException();
63    }
64
65    /**
66     * Parses the clear text URL in {@code str} into a URL object. URL strings
67     * generally have the following format:
68     * <p>
69     * http://www.company.com/java/file1.java#reference
70     * <p>
71     * The string is parsed in HTTP format. If the protocol has a different URL
72     * format this method must be overridden.
73     *
74     * @param url
75     *            the URL to fill in the parsed clear text URL parts.
76     * @param spec
77     *            the URL string that is to be parsed.
78     * @param start
79     *            the string position from where to begin parsing.
80     * @param end
81     *            the string position to stop parsing.
82     * @see #toExternalForm
83     * @see URL
84     */
85    protected void parseURL(URL url, String spec, int start, int end) {
86        if (this != url.streamHandler) {
87            throw new SecurityException("Only a URL's stream handler is permitted to mutate it");
88        }
89        if (end < start) {
90            throw new StringIndexOutOfBoundsException(spec, start, end - start);
91        }
92
93        int fileStart;
94        String authority;
95        String userInfo;
96        String host;
97        int port = -1;
98        String path;
99        String query;
100        String ref;
101        if (spec.regionMatches(start, "//", 0, 2)) {
102            // Parse the authority from the spec.
103            int authorityStart = start + 2;
104            fileStart = findFirstOf(spec, "/?#", authorityStart, end);
105            authority = spec.substring(authorityStart, fileStart);
106            int userInfoEnd = findFirstOf(spec, "@", authorityStart, fileStart);
107            int hostStart;
108            if (userInfoEnd != fileStart) {
109                userInfo = spec.substring(authorityStart, userInfoEnd);
110                hostStart = userInfoEnd + 1;
111            } else {
112                userInfo = null;
113                hostStart = authorityStart;
114            }
115
116            /*
117             * Extract the host and port. The host may be an IPv6 address with
118             * colons like "[::1]", in which case we look for the port delimiter
119             * colon after the ']' character.
120             */
121            int ipv6End = findFirstOf(spec, "]", hostStart, fileStart);
122            int colonSearchFrom = (ipv6End != fileStart) ? ipv6End : hostStart;
123            int hostEnd = findFirstOf(spec, ":", colonSearchFrom, fileStart);
124            host = spec.substring(hostStart, hostEnd);
125            int portStart = hostEnd + 1;
126            if (portStart < fileStart) {
127                port = Integer.parseInt(spec.substring(portStart, fileStart));
128                if (port < 0) {
129                    throw new IllegalArgumentException("port < 0: " + port);
130                }
131            }
132            path = null;
133            query = null;
134            ref = null;
135        } else {
136            // Get the authority from the context URL.
137            fileStart = start;
138            authority = url.getAuthority();
139            userInfo = url.getUserInfo();
140            host = url.getHost();
141            if (host == null) {
142                host = "";
143            }
144            port = url.getPort();
145            path = url.getPath();
146            query = url.getQuery();
147            ref = url.getRef();
148        }
149
150        /*
151         * Extract the path, query and fragment. Each part has its own leading
152         * delimiter character. The query can contain slashes and the fragment
153         * can contain slashes and question marks.
154         *    / path ? query # fragment
155         */
156        int pos = fileStart;
157        while (pos < end) {
158            int nextPos;
159            switch (spec.charAt(pos)) {
160            case '#':
161                nextPos = end;
162                ref = spec.substring(pos + 1, nextPos);
163                break;
164            case '?':
165                nextPos = findFirstOf(spec, "#", pos, end);
166                query = spec.substring(pos + 1, nextPos);
167                ref = null;
168                break;
169            default:
170                nextPos = findFirstOf(spec, "?#", pos, end);
171                path = relativePath(path, spec.substring(pos, nextPos));
172                query = null;
173                ref = null;
174                break;
175            }
176            pos = nextPos;
177        }
178
179        if (path == null) {
180            path = "";
181        }
182
183        path = UrlUtils.authoritySafePath(authority, path);
184
185        setURL(url, url.getProtocol(), host, port, authority, userInfo, path, query, ref);
186    }
187
188    /**
189     * Returns the index of the first char of {@code chars} in {@code string}
190     * bounded between {@code start} and {@code end}. This returns {@code end}
191     * if none of the characters exist in the requested range.
192     */
193    private static int findFirstOf(String string, String chars, int start, int end) {
194        for (int i = start; i < end; i++) {
195            char c = string.charAt(i);
196            if (chars.indexOf(c) != -1) {
197                return i;
198            }
199        }
200        return end;
201    }
202
203    /**
204     * Returns a new path by resolving {@code path} relative to {@code base}.
205     */
206    private static String relativePath(String base, String path) {
207        if (path.startsWith("/")) {
208            return UrlUtils.canonicalizePath(path, true);
209        } else if (base != null) {
210            String combined = base.substring(0, base.lastIndexOf('/') + 1) + path;
211            return UrlUtils.canonicalizePath(combined, true);
212        } else {
213            return path;
214        }
215    }
216
217    /**
218     * Sets the fields of the URL {@code u} to the values of the supplied
219     * arguments.
220     *
221     * @param u
222     *            the non-null URL object to be set.
223     * @param protocol
224     *            the protocol.
225     * @param host
226     *            the host name.
227     * @param port
228     *            the port number.
229     * @param file
230     *            the file component.
231     * @param ref
232     *            the reference.
233     * @deprecated use setURL(URL, String String, int, String, String, String,
234     *             String, String) instead.
235     */
236    @Deprecated
237    protected void setURL(URL u, String protocol, String host, int port,
238            String file, String ref) {
239        if (this != u.streamHandler) {
240            throw new SecurityException();
241        }
242        u.set(protocol, host, port, file, ref);
243    }
244
245    /**
246     * Sets the fields of the URL {@code u} to the values of the supplied
247     * arguments.
248     */
249    protected void setURL(URL u, String protocol, String host, int port,
250            String authority, String userInfo, String path, String query,
251            String ref) {
252        if (this != u.streamHandler) {
253            throw new SecurityException();
254        }
255        u.set(protocol, host, port, authority, userInfo, path, query, ref);
256    }
257
258    /**
259     * Returns the clear text representation of a given URL using HTTP format.
260     *
261     * @param url
262     *            the URL object to be converted.
263     * @return the clear text representation of the specified URL.
264     * @see #parseURL
265     * @see URL#toExternalForm()
266     */
267    protected String toExternalForm(URL url) {
268        return toExternalForm(url, false);
269    }
270
271    String toExternalForm(URL url, boolean escapeIllegalCharacters) {
272        StringBuilder result = new StringBuilder();
273        result.append(url.getProtocol());
274        result.append(':');
275
276        String authority = url.getAuthority();
277        if (authority != null) {
278            result.append("//");
279            if (escapeIllegalCharacters) {
280                URI.AUTHORITY_ENCODER.appendPartiallyEncoded(result, authority);
281            } else {
282                result.append(authority);
283            }
284        }
285
286        String fileAndQuery = url.getFile();
287        if (fileAndQuery != null) {
288            if (escapeIllegalCharacters) {
289                URI.FILE_AND_QUERY_ENCODER.appendPartiallyEncoded(result, fileAndQuery);
290            } else {
291                result.append(fileAndQuery);
292            }
293        }
294
295        String ref = url.getRef();
296        if (ref != null) {
297            result.append('#');
298            if (escapeIllegalCharacters) {
299                URI.ALL_LEGAL_ENCODER.appendPartiallyEncoded(result, ref);
300            } else {
301                result.append(ref);
302            }
303        }
304
305        return result.toString();
306    }
307
308    /**
309     * Returns true if {@code a} and {@code b} have the same protocol, host,
310     * port, file, and reference.
311     */
312    protected boolean equals(URL a, URL b) {
313        return sameFile(a, b)
314                && Objects.equal(a.getRef(), b.getRef())
315                && Objects.equal(a.getQuery(), b.getQuery());
316    }
317
318    /**
319     * Returns the default port of the protocol used by the handled URL. The
320     * default implementation always returns {@code -1}.
321     */
322    protected int getDefaultPort() {
323        return -1;
324    }
325
326    /**
327     * Returns the host address of {@code url}.
328     */
329    protected InetAddress getHostAddress(URL url) {
330        try {
331            String host = url.getHost();
332            if (host == null || host.length() == 0) {
333                return null;
334            }
335            return InetAddress.getByName(host);
336        } catch (UnknownHostException e) {
337            return null;
338        }
339    }
340
341    /**
342     * Returns the hash code of {@code url}.
343     */
344    protected int hashCode(URL url) {
345        return toExternalForm(url).hashCode();
346    }
347
348    /**
349     * Returns true if the hosts of {@code a} and {@code b} are equal.
350     */
351    protected boolean hostsEqual(URL a, URL b) {
352        // URLs with the same case-insensitive host name have equal hosts
353        String aHost = a.getHost();
354        String bHost = b.getHost();
355        return (aHost == bHost) || aHost != null && aHost.equalsIgnoreCase(bHost);
356    }
357
358    /**
359     * Returns true if {@code a} and {@code b} have the same protocol, host,
360     * port and file.
361     */
362    protected boolean sameFile(URL a, URL b) {
363        return Objects.equal(a.getProtocol(), b.getProtocol())
364                && hostsEqual(a, b)
365                && a.getEffectivePort() == b.getEffectivePort()
366                && Objects.equal(a.getFile(), b.getFile());
367    }
368}
369