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.archive;
018
019import java.io.File;
020import java.io.FileOutputStream;
021import java.io.IOException;
022import java.io.InputStream;
023import java.io.OutputStream;
024import java.net.MalformedURLException;
025import java.net.URL;
026import java.util.ArrayList;
027import java.util.Collections;
028import java.util.Enumeration;
029import java.util.Iterator;
030import java.util.List;
031import java.util.UUID;
032import java.util.jar.JarEntry;
033import java.util.jar.Manifest;
034
035import org.springframework.boot.loader.jar.JarFile;
036
037/**
038 * {@link Archive} implementation backed by a {@link JarFile}.
039 *
040 * @author Phillip Webb
041 * @author Andy Wilkinson
042 */
043public class JarFileArchive implements Archive {
044
045        private static final String UNPACK_MARKER = "UNPACK:";
046
047        private static final int BUFFER_SIZE = 32 * 1024;
048
049        private final JarFile jarFile;
050
051        private URL url;
052
053        private File tempUnpackFolder;
054
055        public JarFileArchive(File file) throws IOException {
056                this(file, null);
057        }
058
059        public JarFileArchive(File file, URL url) throws IOException {
060                this(new JarFile(file));
061                this.url = url;
062        }
063
064        public JarFileArchive(JarFile jarFile) {
065                this.jarFile = jarFile;
066        }
067
068        @Override
069        public URL getUrl() throws MalformedURLException {
070                if (this.url != null) {
071                        return this.url;
072                }
073                return this.jarFile.getUrl();
074        }
075
076        @Override
077        public Manifest getManifest() throws IOException {
078                return this.jarFile.getManifest();
079        }
080
081        @Override
082        public List<Archive> getNestedArchives(EntryFilter filter) throws IOException {
083                List<Archive> nestedArchives = new ArrayList<>();
084                for (Entry entry : this) {
085                        if (filter.matches(entry)) {
086                                nestedArchives.add(getNestedArchive(entry));
087                        }
088                }
089                return Collections.unmodifiableList(nestedArchives);
090        }
091
092        @Override
093        public Iterator<Entry> iterator() {
094                return new EntryIterator(this.jarFile.entries());
095        }
096
097        protected Archive getNestedArchive(Entry entry) throws IOException {
098                JarEntry jarEntry = ((JarFileEntry) entry).getJarEntry();
099                if (jarEntry.getComment().startsWith(UNPACK_MARKER)) {
100                        return getUnpackedNestedArchive(jarEntry);
101                }
102                try {
103                        JarFile jarFile = this.jarFile.getNestedJarFile(jarEntry);
104                        return new JarFileArchive(jarFile);
105                }
106                catch (Exception ex) {
107                        throw new IllegalStateException(
108                                        "Failed to get nested archive for entry " + entry.getName(), ex);
109                }
110        }
111
112        private Archive getUnpackedNestedArchive(JarEntry jarEntry) throws IOException {
113                String name = jarEntry.getName();
114                if (name.lastIndexOf('/') != -1) {
115                        name = name.substring(name.lastIndexOf('/') + 1);
116                }
117                File file = new File(getTempUnpackFolder(), name);
118                if (!file.exists() || file.length() != jarEntry.getSize()) {
119                        unpack(jarEntry, file);
120                }
121                return new JarFileArchive(file, file.toURI().toURL());
122        }
123
124        private File getTempUnpackFolder() {
125                if (this.tempUnpackFolder == null) {
126                        File tempFolder = new File(System.getProperty("java.io.tmpdir"));
127                        this.tempUnpackFolder = createUnpackFolder(tempFolder);
128                }
129                return this.tempUnpackFolder;
130        }
131
132        private File createUnpackFolder(File parent) {
133                int attempts = 0;
134                while (attempts++ < 1000) {
135                        String fileName = new File(this.jarFile.getName()).getName();
136                        File unpackFolder = new File(parent,
137                                        fileName + "-spring-boot-libs-" + UUID.randomUUID());
138                        if (unpackFolder.mkdirs()) {
139                                return unpackFolder;
140                        }
141                }
142                throw new IllegalStateException(
143                                "Failed to create unpack folder in directory '" + parent + "'");
144        }
145
146        private void unpack(JarEntry entry, File file) throws IOException {
147                try (InputStream inputStream = this.jarFile.getInputStream(entry);
148                                OutputStream outputStream = new FileOutputStream(file)) {
149                        byte[] buffer = new byte[BUFFER_SIZE];
150                        int bytesRead;
151                        while ((bytesRead = inputStream.read(buffer)) != -1) {
152                                outputStream.write(buffer, 0, bytesRead);
153                        }
154                        outputStream.flush();
155                }
156        }
157
158        @Override
159        public String toString() {
160                try {
161                        return getUrl().toString();
162                }
163                catch (Exception ex) {
164                        return "jar archive";
165                }
166        }
167
168        /**
169         * {@link Archive.Entry} iterator implementation backed by {@link JarEntry}.
170         */
171        private static class EntryIterator implements Iterator<Entry> {
172
173                private final Enumeration<JarEntry> enumeration;
174
175                EntryIterator(Enumeration<JarEntry> enumeration) {
176                        this.enumeration = enumeration;
177                }
178
179                @Override
180                public boolean hasNext() {
181                        return this.enumeration.hasMoreElements();
182                }
183
184                @Override
185                public Entry next() {
186                        return new JarFileEntry(this.enumeration.nextElement());
187                }
188
189                @Override
190                public void remove() {
191                        throw new UnsupportedOperationException("remove");
192                }
193
194        }
195
196        /**
197         * {@link Archive.Entry} implementation backed by a {@link JarEntry}.
198         */
199        private static class JarFileEntry implements Entry {
200
201                private final JarEntry jarEntry;
202
203                JarFileEntry(JarEntry jarEntry) {
204                        this.jarEntry = jarEntry;
205                }
206
207                public JarEntry getJarEntry() {
208                        return this.jarEntry;
209                }
210
211                @Override
212                public boolean isDirectory() {
213                        return this.jarEntry.isDirectory();
214                }
215
216                @Override
217                public String getName() {
218                        return this.jarEntry.getName();
219                }
220
221        }
222
223}