001/*
002 * Copyright 2012-2017 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.boot.loader.jar;
018
019import java.io.File;
020import java.io.IOException;
021import java.io.InputStream;
022import java.lang.ref.SoftReference;
023import java.net.MalformedURLException;
024import java.net.URL;
025import java.net.URLStreamHandler;
026import java.net.URLStreamHandlerFactory;
027import java.util.Enumeration;
028import java.util.Iterator;
029import java.util.jar.JarInputStream;
030import java.util.jar.Manifest;
031import java.util.zip.ZipEntry;
032
033import org.springframework.boot.loader.data.RandomAccessData;
034import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
035import org.springframework.boot.loader.data.RandomAccessDataFile;
036
037/**
038 * Extended variant of {@link java.util.jar.JarFile} that behaves in the same way but
039 * offers the following additional functionality.
040 * <ul>
041 * <li>A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} based
042 * on any directory entry.</li>
043 * <li>A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} for
044 * embedded JAR files (as long as their entry is not compressed).</li>
045 * </ul>
046 *
047 * @author Phillip Webb
048 */
049public class JarFile extends java.util.jar.JarFile {
050
051        private static final String MANIFEST_NAME = "META-INF/MANIFEST.MF";
052
053        private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs";
054
055        private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader";
056
057        private static final AsciiBytes META_INF = new AsciiBytes("META-INF/");
058
059        private static final AsciiBytes SIGNATURE_FILE_EXTENSION = new AsciiBytes(".SF");
060
061        private final RandomAccessDataFile rootFile;
062
063        private final String pathFromRoot;
064
065        private final RandomAccessData data;
066
067        private final JarFileType type;
068
069        private URL url;
070
071        private JarFileEntries entries;
072
073        private SoftReference<Manifest> manifest;
074
075        private boolean signed;
076
077        /**
078         * Create a new {@link JarFile} backed by the specified file.
079         * @param file the root jar file
080         * @throws IOException if the file cannot be read
081         */
082        public JarFile(File file) throws IOException {
083                this(new RandomAccessDataFile(file));
084        }
085
086        /**
087         * Create a new {@link JarFile} backed by the specified file.
088         * @param file the root jar file
089         * @throws IOException if the file cannot be read
090         */
091        JarFile(RandomAccessDataFile file) throws IOException {
092                this(file, "", file, JarFileType.DIRECT);
093        }
094
095        /**
096         * Private constructor used to create a new {@link JarFile} either directly or from a
097         * nested entry.
098         * @param rootFile the root jar file
099         * @param pathFromRoot the name of this file
100         * @param data the underlying data
101         * @param type the type of the jar file
102         * @throws IOException if the file cannot be read
103         */
104        private JarFile(RandomAccessDataFile rootFile, String pathFromRoot,
105                        RandomAccessData data, JarFileType type) throws IOException {
106                this(rootFile, pathFromRoot, data, null, type);
107        }
108
109        private JarFile(RandomAccessDataFile rootFile, String pathFromRoot,
110                        RandomAccessData data, JarEntryFilter filter, JarFileType type)
111                                        throws IOException {
112                super(rootFile.getFile());
113                this.rootFile = rootFile;
114                this.pathFromRoot = pathFromRoot;
115                CentralDirectoryParser parser = new CentralDirectoryParser();
116                this.entries = parser.addVisitor(new JarFileEntries(this, filter));
117                parser.addVisitor(centralDirectoryVisitor());
118                this.data = parser.parse(data, filter == null);
119                this.type = type;
120        }
121
122        private CentralDirectoryVisitor centralDirectoryVisitor() {
123                return new CentralDirectoryVisitor() {
124
125                        @Override
126                        public void visitStart(CentralDirectoryEndRecord endRecord,
127                                        RandomAccessData centralDirectoryData) {
128                        }
129
130                        @Override
131                        public void visitFileHeader(CentralDirectoryFileHeader fileHeader,
132                                        int dataOffset) {
133                                AsciiBytes name = fileHeader.getName();
134                                if (name.startsWith(META_INF)
135                                                && name.endsWith(SIGNATURE_FILE_EXTENSION)) {
136                                        JarFile.this.signed = true;
137                                }
138                        }
139
140                        @Override
141                        public void visitEnd() {
142                        }
143
144                };
145        }
146
147        protected final RandomAccessDataFile getRootJarFile() {
148                return this.rootFile;
149        }
150
151        RandomAccessData getData() {
152                return this.data;
153        }
154
155        @Override
156        public Manifest getManifest() throws IOException {
157                Manifest manifest = (this.manifest == null ? null : this.manifest.get());
158                if (manifest == null) {
159                        if (this.type == JarFileType.NESTED_DIRECTORY) {
160                                manifest = new JarFile(this.getRootJarFile()).getManifest();
161                        }
162                        else {
163                                InputStream inputStream = getInputStream(MANIFEST_NAME,
164                                                ResourceAccess.ONCE);
165                                if (inputStream == null) {
166                                        return null;
167                                }
168                                try {
169                                        manifest = new Manifest(inputStream);
170                                }
171                                finally {
172                                        inputStream.close();
173                                }
174                        }
175                        this.manifest = new SoftReference<Manifest>(manifest);
176                }
177                return manifest;
178        }
179
180        @Override
181        public Enumeration<java.util.jar.JarEntry> entries() {
182                final Iterator<JarEntry> iterator = this.entries.iterator();
183                return new Enumeration<java.util.jar.JarEntry>() {
184
185                        @Override
186                        public boolean hasMoreElements() {
187                                return iterator.hasNext();
188                        }
189
190                        @Override
191                        public java.util.jar.JarEntry nextElement() {
192                                return iterator.next();
193                        }
194
195                };
196        }
197
198        @Override
199        public JarEntry getJarEntry(String name) {
200                return (JarEntry) getEntry(name);
201        }
202
203        public boolean containsEntry(String name) {
204                return this.entries.containsEntry(name);
205        }
206
207        @Override
208        public ZipEntry getEntry(String name) {
209                return this.entries.getEntry(name);
210        }
211
212        @Override
213        public synchronized InputStream getInputStream(ZipEntry ze) throws IOException {
214                return getInputStream(ze, ResourceAccess.PER_READ);
215        }
216
217        public InputStream getInputStream(ZipEntry ze, ResourceAccess access)
218                        throws IOException {
219                if (ze instanceof JarEntry) {
220                        return this.entries.getInputStream((JarEntry) ze, access);
221                }
222                return getInputStream(ze == null ? null : ze.getName(), access);
223        }
224
225        InputStream getInputStream(String name, ResourceAccess access) throws IOException {
226                return this.entries.getInputStream(name, access);
227        }
228
229        /**
230         * Return a nested {@link JarFile} loaded from the specified entry.
231         * @param entry the zip entry
232         * @return a {@link JarFile} for the entry
233         * @throws IOException if the nested jar file cannot be read
234         */
235        public synchronized JarFile getNestedJarFile(final ZipEntry entry)
236                        throws IOException {
237                return getNestedJarFile((JarEntry) entry);
238        }
239
240        /**
241         * Return a nested {@link JarFile} loaded from the specified entry.
242         * @param entry the zip entry
243         * @return a {@link JarFile} for the entry
244         * @throws IOException if the nested jar file cannot be read
245         */
246        public synchronized JarFile getNestedJarFile(JarEntry entry) throws IOException {
247                try {
248                        return createJarFileFromEntry(entry);
249                }
250                catch (Exception ex) {
251                        throw new IOException(
252                                        "Unable to open nested jar file '" + entry.getName() + "'", ex);
253                }
254        }
255
256        private JarFile createJarFileFromEntry(JarEntry entry) throws IOException {
257                if (entry.isDirectory()) {
258                        return createJarFileFromDirectoryEntry(entry);
259                }
260                return createJarFileFromFileEntry(entry);
261        }
262
263        private JarFile createJarFileFromDirectoryEntry(JarEntry entry) throws IOException {
264                final AsciiBytes sourceName = new AsciiBytes(entry.getName());
265                JarEntryFilter filter = new JarEntryFilter() {
266
267                        @Override
268                        public AsciiBytes apply(AsciiBytes name) {
269                                if (name.startsWith(sourceName) && !name.equals(sourceName)) {
270                                        return name.substring(sourceName.length());
271                                }
272                                return null;
273                        }
274
275                };
276                return new JarFile(this.rootFile,
277                                this.pathFromRoot + "!/"
278                                                + entry.getName().substring(0, sourceName.length() - 1),
279                                this.data, filter, JarFileType.NESTED_DIRECTORY);
280        }
281
282        private JarFile createJarFileFromFileEntry(JarEntry entry) throws IOException {
283                if (entry.getMethod() != ZipEntry.STORED) {
284                        throw new IllegalStateException("Unable to open nested entry '"
285                                        + entry.getName() + "'. It has been compressed and nested "
286                                        + "jar files must be stored without compression. Please check the "
287                                        + "mechanism used to create your executable jar file");
288                }
289                RandomAccessData entryData = this.entries.getEntryData(entry.getName());
290                return new JarFile(this.rootFile, this.pathFromRoot + "!/" + entry.getName(),
291                                entryData, JarFileType.NESTED_JAR);
292        }
293
294        @Override
295        public int size() {
296                return (int) this.data.getSize();
297        }
298
299        @Override
300        public void close() throws IOException {
301                super.close();
302                this.rootFile.close();
303        }
304
305        /**
306         * Return a URL that can be used to access this JAR file. NOTE: the specified URL
307         * cannot be serialized and or cloned.
308         * @return the URL
309         * @throws MalformedURLException if the URL is malformed
310         */
311        public URL getUrl() throws MalformedURLException {
312                if (this.url == null) {
313                        Handler handler = new Handler(this);
314                        String file = this.rootFile.getFile().toURI() + this.pathFromRoot + "!/";
315                        file = file.replace("file:////", "file://"); // Fix UNC paths
316                        this.url = new URL("jar", "", -1, file, handler);
317                }
318                return this.url;
319        }
320
321        @Override
322        public String toString() {
323                return getName();
324        }
325
326        @Override
327        public String getName() {
328                return this.rootFile.getFile() + this.pathFromRoot;
329        }
330
331        boolean isSigned() {
332                return this.signed;
333        }
334
335        void setupEntryCertificates(JarEntry entry) {
336                // Fallback to JarInputStream to obtain certificates, not fast but hopefully not
337                // happening that often.
338                try {
339                        JarInputStream inputStream = new JarInputStream(
340                                        getData().getInputStream(ResourceAccess.ONCE));
341                        try {
342                                java.util.jar.JarEntry certEntry = inputStream.getNextJarEntry();
343                                while (certEntry != null) {
344                                        inputStream.closeEntry();
345                                        if (entry.getName().equals(certEntry.getName())) {
346                                                setCertificates(entry, certEntry);
347                                        }
348                                        setCertificates(getJarEntry(certEntry.getName()), certEntry);
349                                        certEntry = inputStream.getNextJarEntry();
350                                }
351                        }
352                        finally {
353                                inputStream.close();
354                        }
355                }
356                catch (IOException ex) {
357                        throw new IllegalStateException(ex);
358                }
359        }
360
361        private void setCertificates(JarEntry entry, java.util.jar.JarEntry certEntry) {
362                if (entry != null) {
363                        entry.setCertificates(certEntry);
364                }
365        }
366
367        public void clearCache() {
368                this.entries.clearCache();
369        }
370
371        protected String getPathFromRoot() {
372                return this.pathFromRoot;
373        }
374
375        JarFileType getType() {
376                return this.type;
377        }
378
379        /**
380         * Register a {@literal 'java.protocol.handler.pkgs'} property so that a
381         * {@link URLStreamHandler} will be located to deal with jar URLs.
382         */
383        public static void registerUrlProtocolHandler() {
384                String handlers = System.getProperty(PROTOCOL_HANDLER, "");
385                System.setProperty(PROTOCOL_HANDLER, ("".equals(handlers) ? HANDLERS_PACKAGE
386                                : handlers + "|" + HANDLERS_PACKAGE));
387                resetCachedUrlHandlers();
388        }
389
390        /**
391         * Reset any cached handlers just in case a jar protocol has already been used. We
392         * reset the handler by trying to set a null {@link URLStreamHandlerFactory} which
393         * should have no effect other than clearing the handlers cache.
394         */
395        private static void resetCachedUrlHandlers() {
396                try {
397                        URL.setURLStreamHandlerFactory(null);
398                }
399                catch (Error ex) {
400                        // Ignore
401                }
402        }
403
404        /**
405         * The type of a {@link JarFile}.
406         */
407        enum JarFileType {
408
409                DIRECT, NESTED_DIRECTORY, NESTED_JAR
410
411        }
412
413}