URLStreamHandler.java revision 5292410e4ebf7fb5149eefd2f52fcb94c46690a6
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        /*
184         * Force the path to start with a '/' if this URL has an authority.
185         * Otherwise they run together like http://android.comindex.html.
186         */
187        if (authority != null && !authority.isEmpty() && !path.startsWith("/") && !path.isEmpty()) {
188            path = "/" + path;
189        }
190
191        setURL(url, url.getProtocol(), host, port, authority, userInfo, path, query, ref);
192    }
193
194    /**
195     * Returns the index of the first char of {@code chars} in {@code string}
196     * bounded between {@code start} and {@code end}. This returns {@code end}
197     * if none of the characters exist in the requested range.
198     */
199    private static int findFirstOf(String string, String chars, int start, int end) {
200        for (int i = start; i < end; i++) {
201            char c = string.charAt(i);
202            if (chars.indexOf(c) != -1) {
203                return i;
204            }
205        }
206        return end;
207    }
208
209    /**
210     * Returns a new path by resolving {@code path} relative to {@code base}.
211     */
212    private static String relativePath(String base, String path) {
213        if (path.startsWith("/")) {
214            return UrlUtils.canonicalizePath(path);
215        } else if (base != null) {
216            String combined = base.substring(0, base.lastIndexOf('/') + 1) + path;
217            return UrlUtils.canonicalizePath(combined);
218        } else {
219            return path;
220        }
221    }
222
223    /**
224     * Sets the fields of the URL {@code u} to the values of the supplied
225     * arguments.
226     *
227     * @param u
228     *            the non-null URL object to be set.
229     * @param protocol
230     *            the protocol.
231     * @param host
232     *            the host name.
233     * @param port
234     *            the port number.
235     * @param file
236     *            the file component.
237     * @param ref
238     *            the reference.
239     * @deprecated use setURL(URL, String String, int, String, String, String,
240     *             String, String) instead.
241     */
242    @Deprecated
243    protected void setURL(URL u, String protocol, String host, int port,
244            String file, String ref) {
245        if (this != u.streamHandler) {
246            throw new SecurityException();
247        }
248        u.set(protocol, host, port, file, ref);
249    }
250
251    /**
252     * Sets the fields of the URL {@code u} to the values of the supplied
253     * arguments.
254     */
255    protected void setURL(URL u, String protocol, String host, int port,
256            String authority, String userInfo, String path, String query,
257            String ref) {
258        if (this != u.streamHandler) {
259            throw new SecurityException();
260        }
261        u.set(protocol, host, port, authority, userInfo, path, query, ref);
262    }
263
264    /**
265     * Returns the clear text representation of a given URL using HTTP format.
266     *
267     * @param url
268     *            the URL object to be converted.
269     * @return the clear text representation of the specified URL.
270     * @see #parseURL
271     * @see URL#toExternalForm()
272     */
273    protected String toExternalForm(URL url) {
274        return toExternalForm(url, false);
275    }
276
277    String toExternalForm(URL url, boolean escapeIllegalCharacters) {
278        StringBuilder result = new StringBuilder();
279        result.append(url.getProtocol());
280        result.append(':');
281
282        String authority = url.getAuthority();
283        if (authority != null) {
284            result.append("//");
285            if (escapeIllegalCharacters) {
286                URI.AUTHORITY_ENCODER.appendPartiallyEncoded(result, authority);
287            } else {
288                result.append(authority);
289            }
290        }
291
292        String fileAndQuery = url.getFile();
293        if (fileAndQuery != null) {
294            if (escapeIllegalCharacters) {
295                URI.FILE_AND_QUERY_ENCODER.appendPartiallyEncoded(result, fileAndQuery);
296            } else {
297                result.append(fileAndQuery);
298            }
299        }
300
301        String ref = url.getRef();
302        if (ref != null) {
303            result.append('#');
304            if (escapeIllegalCharacters) {
305                URI.ALL_LEGAL_ENCODER.appendPartiallyEncoded(result, ref);
306            } else {
307                result.append(ref);
308            }
309        }
310
311        return result.toString();
312    }
313
314    /**
315     * Returns true if {@code a} and {@code b} have the same protocol, host,
316     * port, file, and reference.
317     */
318    protected boolean equals(URL a, URL b) {
319        return sameFile(a, b)
320                && Objects.equal(a.getRef(), b.getRef())
321                && Objects.equal(a.getQuery(), b.getQuery());
322    }
323
324    /**
325     * Returns the default port of the protocol used by the handled URL. The
326     * default implementation always returns {@code -1}.
327     */
328    protected int getDefaultPort() {
329        return -1;
330    }
331
332    /**
333     * Returns the host address of {@code url}.
334     */
335    protected InetAddress getHostAddress(URL url) {
336        try {
337            String host = url.getHost();
338            if (host == null || host.length() == 0) {
339                return null;
340            }
341            return InetAddress.getByName(host);
342        } catch (UnknownHostException e) {
343            return null;
344        }
345    }
346
347    /**
348     * Returns the hash code of {@code url}.
349     */
350    protected int hashCode(URL url) {
351        return toExternalForm(url).hashCode();
352    }
353
354    /**
355     * Returns true if the hosts of {@code a} and {@code b} are equal.
356     */
357    protected boolean hostsEqual(URL a, URL b) {
358        // URLs with the same case-insensitive host name have equal hosts
359        String aHost = a.getHost();
360        String bHost = b.getHost();
361        return (aHost == bHost) || aHost != null && aHost.equalsIgnoreCase(bHost);
362    }
363
364    /**
365     * Returns true if {@code a} and {@code b} have the same protocol, host,
366     * port and file.
367     */
368    protected boolean sameFile(URL a, URL b) {
369        return Objects.equal(a.getProtocol(), b.getProtocol())
370                && hostsEqual(a, b)
371                && a.getEffectivePort() == b.getEffectivePort()
372                && Objects.equal(a.getFile(), b.getFile());
373    }
374}
375