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 = UrlUtils.findFirstOf(spec, "/?#", authorityStart, end);
105            authority = spec.substring(authorityStart, fileStart);
106            int userInfoEnd = UrlUtils.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 colonSearchFrom = hostStart;
122            int ipv6End = UrlUtils.findFirstOf(spec, "]", hostStart, fileStart);
123            if (ipv6End != fileStart) {
124                if (UrlUtils.findFirstOf(spec, ":", hostStart, ipv6End) == ipv6End) {
125                    throw new IllegalArgumentException("Expected an IPv6 address: "
126                            + spec.substring(hostStart, ipv6End + 1));
127                }
128                colonSearchFrom = ipv6End;
129            }
130            int hostEnd = UrlUtils.findFirstOf(spec, ":", colonSearchFrom, fileStart);
131            host = spec.substring(hostStart, hostEnd);
132            int portStart = hostEnd + 1;
133            if (portStart < fileStart) {
134                port = Integer.parseInt(spec.substring(portStart, fileStart));
135                if (port < 0) {
136                    throw new IllegalArgumentException("port < 0: " + port);
137                }
138            }
139            path = null;
140            query = null;
141            ref = null;
142        } else {
143            // Get the authority from the context URL.
144            fileStart = start;
145            authority = url.getAuthority();
146            userInfo = url.getUserInfo();
147            host = url.getHost();
148            if (host == null) {
149                host = "";
150            }
151            port = url.getPort();
152            path = url.getPath();
153            query = url.getQuery();
154            ref = url.getRef();
155        }
156
157        /*
158         * Extract the path, query and fragment. Each part has its own leading
159         * delimiter character. The query can contain slashes and the fragment
160         * can contain slashes and question marks.
161         *    / path ? query # fragment
162         */
163        int pos = fileStart;
164        while (pos < end) {
165            int nextPos;
166            switch (spec.charAt(pos)) {
167            case '#':
168                nextPos = end;
169                ref = spec.substring(pos + 1, nextPos);
170                break;
171            case '?':
172                nextPos = UrlUtils.findFirstOf(spec, "#", pos, end);
173                query = spec.substring(pos + 1, nextPos);
174                ref = null;
175                break;
176            default:
177                nextPos = UrlUtils.findFirstOf(spec, "?#", pos, end);
178                path = relativePath(path, spec.substring(pos, nextPos));
179                query = null;
180                ref = null;
181                break;
182            }
183            pos = nextPos;
184        }
185
186        if (path == null) {
187            path = "";
188        }
189
190        path = UrlUtils.authoritySafePath(authority, path);
191
192        setURL(url, url.getProtocol(), host, port, authority, userInfo, path, query, ref);
193    }
194
195    /**
196     * Returns a new path by resolving {@code path} relative to {@code base}.
197     */
198    private static String relativePath(String base, String path) {
199        if (path.startsWith("/")) {
200            return UrlUtils.canonicalizePath(path, true);
201        } else if (base != null) {
202            String combined = base.substring(0, base.lastIndexOf('/') + 1) + path;
203            return UrlUtils.canonicalizePath(combined, true);
204        } else {
205            return path;
206        }
207    }
208
209    /**
210     * Sets the fields of the URL {@code u} to the values of the supplied
211     * arguments.
212     *
213     * @param u
214     *            the non-null URL object to be set.
215     * @param protocol
216     *            the protocol.
217     * @param host
218     *            the host name.
219     * @param port
220     *            the port number.
221     * @param file
222     *            the file component.
223     * @param ref
224     *            the reference.
225     * @deprecated use setURL(URL, String String, int, String, String, String,
226     *             String, String) instead.
227     */
228    @Deprecated
229    protected void setURL(URL u, String protocol, String host, int port,
230            String file, String ref) {
231        if (this != u.streamHandler) {
232            throw new SecurityException();
233        }
234        u.set(protocol, host, port, file, ref);
235    }
236
237    /**
238     * Sets the fields of the URL {@code u} to the values of the supplied
239     * arguments.
240     */
241    protected void setURL(URL u, String protocol, String host, int port,
242            String authority, String userInfo, String path, String query,
243            String ref) {
244        if (this != u.streamHandler) {
245            throw new SecurityException();
246        }
247        u.set(protocol, host, port, authority, userInfo, path, query, ref);
248    }
249
250    /**
251     * Returns the clear text representation of a given URL using HTTP format.
252     *
253     * @param url
254     *            the URL object to be converted.
255     * @return the clear text representation of the specified URL.
256     * @see #parseURL
257     * @see URL#toExternalForm()
258     */
259    protected String toExternalForm(URL url) {
260        return toExternalForm(url, false);
261    }
262
263    String toExternalForm(URL url, boolean escapeIllegalCharacters) {
264        StringBuilder result = new StringBuilder();
265        result.append(url.getProtocol());
266        result.append(':');
267
268        String authority = url.getAuthority();
269        if (authority != null) {
270            result.append("//");
271            if (escapeIllegalCharacters) {
272                URI.AUTHORITY_ENCODER.appendPartiallyEncoded(result, authority);
273            } else {
274                result.append(authority);
275            }
276        }
277
278        String fileAndQuery = url.getFile();
279        if (fileAndQuery != null) {
280            if (escapeIllegalCharacters) {
281                URI.FILE_AND_QUERY_ENCODER.appendPartiallyEncoded(result, fileAndQuery);
282            } else {
283                result.append(fileAndQuery);
284            }
285        }
286
287        String ref = url.getRef();
288        if (ref != null) {
289            result.append('#');
290            if (escapeIllegalCharacters) {
291                URI.ALL_LEGAL_ENCODER.appendPartiallyEncoded(result, ref);
292            } else {
293                result.append(ref);
294            }
295        }
296
297        return result.toString();
298    }
299
300    /**
301     * Returns true if {@code a} and {@code b} have the same protocol, host,
302     * port, file, and reference.
303     */
304    protected boolean equals(URL a, URL b) {
305        return sameFile(a, b)
306                && Objects.equal(a.getRef(), b.getRef())
307                && Objects.equal(a.getQuery(), b.getQuery());
308    }
309
310    /**
311     * Returns the default port of the protocol used by the handled URL. The
312     * default implementation always returns {@code -1}.
313     */
314    protected int getDefaultPort() {
315        return -1;
316    }
317
318    /**
319     * Returns the host address of {@code url}.
320     */
321    protected InetAddress getHostAddress(URL url) {
322        try {
323            String host = url.getHost();
324            if (host == null || host.length() == 0) {
325                return null;
326            }
327            return InetAddress.getByName(host);
328        } catch (UnknownHostException e) {
329            return null;
330        }
331    }
332
333    /**
334     * Returns the hash code of {@code url}.
335     */
336    protected int hashCode(URL url) {
337        return toExternalForm(url).hashCode();
338    }
339
340    /**
341     * Returns true if the hosts of {@code a} and {@code b} are equal.
342     */
343    protected boolean hostsEqual(URL a, URL b) {
344        // URLs with the same case-insensitive host name have equal hosts
345        String aHost = a.getHost();
346        String bHost = b.getHost();
347        return (aHost == bHost) || aHost != null && aHost.equalsIgnoreCase(bHost);
348    }
349
350    /**
351     * Returns true if {@code a} and {@code b} have the same protocol, host,
352     * port and file.
353     */
354    protected boolean sameFile(URL a, URL b) {
355        return Objects.equal(a.getProtocol(), b.getProtocol())
356                && hostsEqual(a, b)
357                && a.getEffectivePort() == b.getEffectivePort()
358                && Objects.equal(a.getFile(), b.getFile());
359    }
360}
361