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