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