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