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 libcore.net.url;
19
20import java.io.File;
21import java.io.FileNotFoundException;
22import java.io.FileOutputStream;
23import java.io.FilterInputStream;
24import java.io.IOException;
25import java.io.InputStream;
26import java.net.ContentHandler;
27import java.net.ContentHandlerFactory;
28import java.net.JarURLConnection;
29import java.net.MalformedURLException;
30import java.net.URL;
31import java.security.Permission;
32import java.util.HashMap;
33import java.util.Iterator;
34import java.util.Map;
35import java.util.Set;
36import java.util.jar.JarEntry;
37import java.util.jar.JarFile;
38import java.util.zip.ZipFile;
39import libcore.net.UriCodec;
40
41/**
42 * This subclass extends {@code URLConnection}.
43 * <p>
44 *
45 * This class is responsible for connecting and retrieving resources from a Jar
46 * file which can be anywhere that can be referred to by an URL.
47 */
48public class JarURLConnectionImpl extends JarURLConnection {
49
50    static HashMap<URL, JarFile> jarCache = new HashMap<URL, JarFile>();
51
52    private URL jarFileURL;
53
54    private InputStream jarInput;
55
56    private JarFile jarFile;
57
58    private JarEntry jarEntry;
59
60    private boolean closed;
61
62    /**
63     * @param url
64     *            the URL of the JAR
65     * @throws MalformedURLException
66     *             if the URL is malformed
67     * @throws IOException
68     *             if there is a problem opening the connection.
69     */
70    public JarURLConnectionImpl(URL url) throws MalformedURLException,
71            IOException {
72        super(url);
73        jarFileURL = getJarFileURL();
74        jarFileURLConnection = jarFileURL.openConnection();
75    }
76
77    /**
78     * @see java.net.URLConnection#connect()
79     */
80    @Override
81    public void connect() throws IOException {
82        if (!connected) {
83            findJarFile(); // ensure the file can be found
84            findJarEntry(); // ensure the entry, if any, can be found
85            connected = true;
86        }
87    }
88
89    /**
90     * Returns the Jar file referred by this {@code URLConnection}.
91     *
92     * @return the JAR file referenced by this connection
93     *
94     * @throws IOException
95     *             thrown if an IO error occurs while connecting to the
96     *             resource.
97     */
98    @Override
99    public JarFile getJarFile() throws IOException {
100        connect();
101        return jarFile;
102    }
103
104    /**
105     * Returns the Jar file referred by this {@code URLConnection}
106     *
107     * @throws IOException
108     *             if an IO error occurs while connecting to the resource.
109     */
110    private void findJarFile() throws IOException {
111        JarFile jar = null;
112        if (getUseCaches()) {
113            synchronized (jarCache) {
114                jarFile = jarCache.get(jarFileURL);
115            }
116            if (jarFile == null) {
117                jar = openJarFile();
118                synchronized (jarCache) {
119                    jarFile = jarCache.get(jarFileURL);
120                    if (jarFile == null) {
121                        jarCache.put(jarFileURL, jar);
122                        jarFile = jar;
123                    } else {
124                        jar.close();
125                    }
126                }
127            }
128        } else {
129            jarFile = openJarFile();
130        }
131
132        if (jarFile == null) {
133            throw new IOException();
134        }
135    }
136
137    JarFile openJarFile() throws IOException {
138        if (jarFileURL.getProtocol().equals("file")) {
139            String decodedFile = UriCodec.decode(jarFileURL.getFile());
140            return new JarFile(new File(decodedFile), true, ZipFile.OPEN_READ);
141        } else {
142            final InputStream is = jarFileURL.openConnection().getInputStream();
143            try {
144                FileOutputStream fos = null;
145                JarFile result = null;
146                try {
147                    File tempJar = File.createTempFile("hyjar_", ".tmp", null);
148                    tempJar.deleteOnExit();
149                    fos = new FileOutputStream(tempJar);
150                    byte[] buf = new byte[4096];
151                    int nbytes = 0;
152                    while ((nbytes = is.read(buf)) > -1) {
153                        fos.write(buf, 0, nbytes);
154                    }
155                    fos.close();
156                    return new JarFile(tempJar, true, ZipFile.OPEN_READ | ZipFile.OPEN_DELETE);
157                } catch (IOException e) {
158                    return null;
159                } finally {
160                    if (fos != null) {
161                        try {
162                            fos.close();
163                        } catch (IOException ex) {
164                            return null;
165                        }
166                    }
167                }
168            } finally {
169                if (is != null) {
170                    is.close();
171                }
172            }
173        }
174    }
175
176    /**
177     * Returns the JarEntry of the entry referenced by this {@code
178     * URLConnection}.
179     *
180     * @return the JarEntry referenced
181     *
182     * @throws IOException
183     *             if an IO error occurs while getting the entry
184     */
185    @Override
186    public JarEntry getJarEntry() throws IOException {
187        connect();
188        return jarEntry;
189
190    }
191
192    /**
193     * Look up the JarEntry of the entry referenced by this {@code
194     * URLConnection}.
195     */
196    private void findJarEntry() throws IOException {
197        if (getEntryName() == null) {
198            return;
199        }
200        jarEntry = jarFile.getJarEntry(getEntryName());
201        if (jarEntry == null) {
202            throw new FileNotFoundException(getEntryName());
203        }
204    }
205
206    /**
207     * Creates an input stream for reading from this URL Connection.
208     *
209     * @return the input stream
210     *
211     * @throws IOException
212     *             if an IO error occurs while connecting to the resource.
213     */
214    @Override
215    public InputStream getInputStream() throws IOException {
216        if (closed) {
217            throw new IllegalStateException("JarURLConnection InputStream has been closed");
218        }
219        connect();
220        if (jarInput != null) {
221            return jarInput;
222        }
223        if (jarEntry == null) {
224            throw new IOException("Jar entry not specified");
225        }
226        return jarInput = new JarURLConnectionInputStream(jarFile
227                .getInputStream(jarEntry), jarFile);
228    }
229
230    /**
231     * Returns the content type of the resource. For jar file itself
232     * "x-java/jar" should be returned, for jar entries the content type of the
233     * entry should be returned. Returns non-null results ("content/unknown" for
234     * unknown types).
235     *
236     * @return the content type
237     */
238    @Override
239    public String getContentType() {
240        if (url.getFile().endsWith("!/")) {
241            // the type for jar file itself is always "x-java/jar"
242            return "x-java/jar";
243        }
244        String cType = null;
245        String entryName = getEntryName();
246
247        if (entryName != null) {
248            // if there is an Jar Entry, get the content type from the name
249            cType = guessContentTypeFromName(entryName);
250        } else {
251            try {
252                connect();
253                cType = jarFileURLConnection.getContentType();
254            } catch (IOException ioe) {
255                // Ignore
256            }
257        }
258        if (cType == null) {
259            cType = "content/unknown";
260        }
261        return cType;
262    }
263
264    /**
265     * Returns the content length of the resource. Test cases reveal that if the
266     * URL is referring to a Jar file, this method answers a content-length
267     * returned by URLConnection. For jar entry it should return it's size.
268     * Otherwise, it will return -1.
269     *
270     * @return the content length
271     */
272    @Override
273    public int getContentLength() {
274        try {
275            connect();
276            if (jarEntry == null) {
277                return jarFileURLConnection.getContentLength();
278            }
279            return (int) getJarEntry().getSize();
280        } catch (IOException e) {
281            // Ignored
282        }
283        return -1;
284    }
285
286    /**
287     * Returns the object pointed by this {@code URL}. If this URLConnection is
288     * pointing to a Jar File (no Jar Entry), this method will return a {@code
289     * JarFile} If there is a Jar Entry, it will return the object corresponding
290     * to the Jar entry content type.
291     *
292     * @return a non-null object
293     *
294     * @throws IOException
295     *             if an IO error occurred
296     *
297     * @see ContentHandler
298     * @see ContentHandlerFactory
299     * @see java.io.IOException
300     * @see #setContentHandlerFactory(ContentHandlerFactory)
301     */
302    @Override
303    public Object getContent() throws IOException {
304        connect();
305        // if there is no Jar Entry, return a JarFile
306        if (jarEntry == null) {
307            return jarFile;
308        }
309        return super.getContent();
310    }
311
312    /**
313     * Returns the permission, in this case the subclass, FilePermission object
314     * which represents the permission necessary for this URLConnection to
315     * establish the connection.
316     *
317     * @return the permission required for this URLConnection.
318     *
319     * @throws IOException
320     *             thrown when an IO exception occurs while creating the
321     *             permission.
322     */
323
324    @Override
325    public Permission getPermission() throws IOException {
326        return jarFileURLConnection.getPermission();
327    }
328
329    @Override
330    public boolean getUseCaches() {
331        return jarFileURLConnection.getUseCaches();
332    }
333
334    @Override
335    public void setUseCaches(boolean usecaches) {
336        jarFileURLConnection.setUseCaches(usecaches);
337    }
338
339    @Override
340    public boolean getDefaultUseCaches() {
341        return jarFileURLConnection.getDefaultUseCaches();
342    }
343
344    @Override
345    public void setDefaultUseCaches(boolean defaultusecaches) {
346        jarFileURLConnection.setDefaultUseCaches(defaultusecaches);
347    }
348
349    /**
350     * Closes the cached files.
351     */
352    public static void closeCachedFiles() {
353        Set<Map.Entry<URL, JarFile>> s = jarCache.entrySet();
354        synchronized (jarCache) {
355            Iterator<Map.Entry<URL, JarFile>> i = s.iterator();
356            while (i.hasNext()) {
357                try {
358                    ZipFile zip = i.next().getValue();
359                    if (zip != null) {
360                        zip.close();
361                    }
362                } catch (IOException e) {
363                    // Ignored
364                }
365            }
366        }
367    }
368
369    private class JarURLConnectionInputStream extends FilterInputStream {
370        final JarFile jarFile;
371
372        protected JarURLConnectionInputStream(InputStream in, JarFile file) {
373            super(in);
374            jarFile = file;
375        }
376
377        @Override
378        public void close() throws IOException {
379            super.close();
380            if (!getUseCaches()) {
381                closed = true;
382                jarFile.close();
383            }
384        }
385    }
386}
387