001/*
002 * Copyright 2012-2016 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.tools;
018
019import java.io.BufferedInputStream;
020import java.io.ByteArrayInputStream;
021import java.io.File;
022import java.io.FileInputStream;
023import java.io.FileNotFoundException;
024import java.io.FileOutputStream;
025import java.io.FilterInputStream;
026import java.io.IOException;
027import java.io.InputStream;
028import java.io.OutputStream;
029import java.net.URL;
030import java.nio.file.Files;
031import java.nio.file.Path;
032import java.nio.file.attribute.PosixFilePermission;
033import java.util.Arrays;
034import java.util.Enumeration;
035import java.util.HashSet;
036import java.util.Set;
037import java.util.jar.JarEntry;
038import java.util.jar.JarFile;
039import java.util.jar.JarInputStream;
040import java.util.jar.JarOutputStream;
041import java.util.jar.Manifest;
042import java.util.zip.CRC32;
043import java.util.zip.ZipEntry;
044
045import org.springframework.lang.UsesJava7;
046
047/**
048 * Writes JAR content, ensuring valid directory entries are always create and duplicate
049 * items are ignored.
050 *
051 * @author Phillip Webb
052 * @author Andy Wilkinson
053 */
054public class JarWriter implements LoaderClassesWriter {
055
056        private static final String NESTED_LOADER_JAR = "META-INF/loader/spring-boot-loader.jar";
057
058        private static final int BUFFER_SIZE = 32 * 1024;
059
060        private final JarOutputStream jarOutput;
061
062        private final Set<String> writtenEntries = new HashSet<String>();
063
064        /**
065         * Create a new {@link JarWriter} instance.
066         * @param file the file to write
067         * @throws IOException if the file cannot be opened
068         * @throws FileNotFoundException if the file cannot be found
069         */
070        public JarWriter(File file) throws FileNotFoundException, IOException {
071                this(file, null);
072        }
073
074        /**
075         * Create a new {@link JarWriter} instance.
076         * @param file the file to write
077         * @param launchScript an optional launch script to prepend to the front of the jar
078         * @throws IOException if the file cannot be opened
079         * @throws FileNotFoundException if the file cannot be found
080         */
081        public JarWriter(File file, LaunchScript launchScript)
082                        throws FileNotFoundException, IOException {
083                FileOutputStream fileOutputStream = new FileOutputStream(file);
084                if (launchScript != null) {
085                        fileOutputStream.write(launchScript.toByteArray());
086                        setExecutableFilePermission(file);
087                }
088                this.jarOutput = new JarOutputStream(fileOutputStream);
089        }
090
091        @UsesJava7
092        private void setExecutableFilePermission(File file) {
093                try {
094                        Path path = file.toPath();
095                        Set<PosixFilePermission> permissions = new HashSet<PosixFilePermission>(
096                                        Files.getPosixFilePermissions(path));
097                        permissions.add(PosixFilePermission.OWNER_EXECUTE);
098                        Files.setPosixFilePermissions(path, permissions);
099                }
100                catch (Throwable ex) {
101                        // Ignore and continue creating the jar
102                }
103        }
104
105        /**
106         * Write the specified manifest.
107         * @param manifest the manifest to write
108         * @throws IOException of the manifest cannot be written
109         */
110        public void writeManifest(final Manifest manifest) throws IOException {
111                JarEntry entry = new JarEntry("META-INF/MANIFEST.MF");
112                writeEntry(entry, new EntryWriter() {
113                        @Override
114                        public void write(OutputStream outputStream) throws IOException {
115                                manifest.write(outputStream);
116                        }
117                });
118        }
119
120        /**
121         * Write all entries from the specified jar file.
122         * @param jarFile the source jar file
123         * @throws IOException if the entries cannot be written
124         */
125        public void writeEntries(JarFile jarFile) throws IOException {
126                this.writeEntries(jarFile, new IdentityEntryTransformer());
127        }
128
129        void writeEntries(JarFile jarFile, EntryTransformer entryTransformer)
130                        throws IOException {
131                Enumeration<JarEntry> entries = jarFile.entries();
132                while (entries.hasMoreElements()) {
133                        JarEntry entry = entries.nextElement();
134                        ZipHeaderPeekInputStream inputStream = new ZipHeaderPeekInputStream(
135                                        jarFile.getInputStream(entry));
136                        try {
137                                if (inputStream.hasZipHeader() && entry.getMethod() != ZipEntry.STORED) {
138                                        new CrcAndSize(inputStream).setupStoredEntry(entry);
139                                        inputStream.close();
140                                        inputStream = new ZipHeaderPeekInputStream(
141                                                        jarFile.getInputStream(entry));
142                                }
143                                EntryWriter entryWriter = new InputStreamEntryWriter(inputStream, true);
144                                JarEntry transformedEntry = entryTransformer.transform(entry);
145                                if (transformedEntry != null) {
146                                        writeEntry(transformedEntry, entryWriter);
147                                }
148                        }
149                        finally {
150                                inputStream.close();
151                        }
152                }
153        }
154
155        /**
156         * Writes an entry. The {@code inputStream} is closed once the entry has been written
157         * @param entryName The name of the entry
158         * @param inputStream The stream from which the entry's data can be read
159         * @throws IOException if the write fails
160         */
161        @Override
162        public void writeEntry(String entryName, InputStream inputStream) throws IOException {
163                JarEntry entry = new JarEntry(entryName);
164                writeEntry(entry, new InputStreamEntryWriter(inputStream, true));
165        }
166
167        /**
168         * Write a nested library.
169         * @param destination the destination of the library
170         * @param library the library
171         * @throws IOException if the write fails
172         */
173        public void writeNestedLibrary(String destination, Library library)
174                        throws IOException {
175                File file = library.getFile();
176                JarEntry entry = new JarEntry(destination + library.getName());
177                entry.setTime(getNestedLibraryTime(file));
178                if (library.isUnpackRequired()) {
179                        entry.setComment("UNPACK:" + FileUtils.sha1Hash(file));
180                }
181                new CrcAndSize(file).setupStoredEntry(entry);
182                writeEntry(entry, new InputStreamEntryWriter(new FileInputStream(file), true));
183        }
184
185        private long getNestedLibraryTime(File file) {
186                try {
187                        JarFile jarFile = new JarFile(file);
188                        try {
189                                Enumeration<JarEntry> entries = jarFile.entries();
190                                while (entries.hasMoreElements()) {
191                                        JarEntry entry = entries.nextElement();
192                                        if (!entry.isDirectory()) {
193                                                return entry.getTime();
194                                        }
195                                }
196                        }
197                        finally {
198                                jarFile.close();
199                        }
200                }
201                catch (Exception ex) {
202                        // Ignore and just use the source file timestamp
203                }
204                return file.lastModified();
205        }
206
207        /**
208         * Write the required spring-boot-loader classes to the JAR.
209         * @throws IOException if the classes cannot be written
210         */
211        @Override
212        public void writeLoaderClasses() throws IOException {
213                writeLoaderClasses(NESTED_LOADER_JAR);
214        }
215
216        /**
217         * Write the required spring-boot-loader classes to the JAR.
218         * @param loaderJarResourceName the name of the resource containing the loader classes
219         * to be written
220         * @throws IOException if the classes cannot be written
221         */
222        @Override
223        public void writeLoaderClasses(String loaderJarResourceName) throws IOException {
224                URL loaderJar = getClass().getClassLoader().getResource(loaderJarResourceName);
225                JarInputStream inputStream = new JarInputStream(
226                                new BufferedInputStream(loaderJar.openStream()));
227                JarEntry entry;
228                while ((entry = inputStream.getNextJarEntry()) != null) {
229                        if (entry.getName().endsWith(".class")) {
230                                writeEntry(entry, new InputStreamEntryWriter(inputStream, false));
231                        }
232                }
233                inputStream.close();
234        }
235
236        /**
237         * Close the writer.
238         * @throws IOException if the file cannot be closed
239         */
240        public void close() throws IOException {
241                this.jarOutput.close();
242        }
243
244        /**
245         * Perform the actual write of a {@link JarEntry}. All other {@code write} method
246         * delegate to this one.
247         * @param entry the entry to write
248         * @param entryWriter the entry writer or {@code null} if there is no content
249         * @throws IOException in case of I/O errors
250         */
251        private void writeEntry(JarEntry entry, EntryWriter entryWriter) throws IOException {
252                String parent = entry.getName();
253                if (parent.endsWith("/")) {
254                        parent = parent.substring(0, parent.length() - 1);
255                }
256                if (parent.lastIndexOf("/") != -1) {
257                        parent = parent.substring(0, parent.lastIndexOf("/") + 1);
258                        if (parent.length() > 0) {
259                                writeEntry(new JarEntry(parent), null);
260                        }
261                }
262
263                if (this.writtenEntries.add(entry.getName())) {
264                        this.jarOutput.putNextEntry(entry);
265                        if (entryWriter != null) {
266                                entryWriter.write(this.jarOutput);
267                        }
268                        this.jarOutput.closeEntry();
269                }
270        }
271
272        /**
273         * Interface used to write jar entry date.
274         */
275        private interface EntryWriter {
276
277                /**
278                 * Write entry data to the specified output stream.
279                 * @param outputStream the destination for the data
280                 * @throws IOException in case of I/O errors
281                 */
282                void write(OutputStream outputStream) throws IOException;
283
284        }
285
286        /**
287         * {@link EntryWriter} that writes content from an {@link InputStream}.
288         */
289        private static class InputStreamEntryWriter implements EntryWriter {
290
291                private final InputStream inputStream;
292
293                private final boolean close;
294
295                InputStreamEntryWriter(InputStream inputStream, boolean close) {
296                        this.inputStream = inputStream;
297                        this.close = close;
298                }
299
300                @Override
301                public void write(OutputStream outputStream) throws IOException {
302                        byte[] buffer = new byte[BUFFER_SIZE];
303                        int bytesRead;
304                        while ((bytesRead = this.inputStream.read(buffer)) != -1) {
305                                outputStream.write(buffer, 0, bytesRead);
306                        }
307                        outputStream.flush();
308                        if (this.close) {
309                                this.inputStream.close();
310                        }
311                }
312
313        }
314
315        /**
316         * {@link InputStream} that can peek ahead at zip header bytes.
317         */
318        private static class ZipHeaderPeekInputStream extends FilterInputStream {
319
320                private static final byte[] ZIP_HEADER = new byte[] { 0x50, 0x4b, 0x03, 0x04 };
321
322                private final byte[] header;
323
324                private ByteArrayInputStream headerStream;
325
326                protected ZipHeaderPeekInputStream(InputStream in) throws IOException {
327                        super(in);
328                        this.header = new byte[4];
329                        int len = in.read(this.header);
330                        this.headerStream = new ByteArrayInputStream(this.header, 0, len);
331                }
332
333                @Override
334                public int read() throws IOException {
335                        int read = (this.headerStream == null ? -1 : this.headerStream.read());
336                        if (read != -1) {
337                                this.headerStream = null;
338                                return read;
339                        }
340                        return super.read();
341                }
342
343                @Override
344                public int read(byte[] b) throws IOException {
345                        return read(b, 0, b.length);
346                }
347
348                @Override
349                public int read(byte[] b, int off, int len) throws IOException {
350                        int read = (this.headerStream == null ? -1
351                                        : this.headerStream.read(b, off, len));
352                        if (read != -1) {
353                                this.headerStream = null;
354                                return read;
355                        }
356                        return super.read(b, off, len);
357                }
358
359                public boolean hasZipHeader() {
360                        return Arrays.equals(this.header, ZIP_HEADER);
361                }
362
363        }
364
365        /**
366         * Data holder for CRC and Size.
367         */
368        private static class CrcAndSize {
369
370                private final CRC32 crc = new CRC32();
371
372                private long size;
373
374                CrcAndSize(File file) throws IOException {
375                        FileInputStream inputStream = new FileInputStream(file);
376                        try {
377                                load(inputStream);
378                        }
379                        finally {
380                                inputStream.close();
381                        }
382                }
383
384                CrcAndSize(InputStream inputStream) throws IOException {
385                        load(inputStream);
386                }
387
388                private void load(InputStream inputStream) throws IOException {
389                        byte[] buffer = new byte[BUFFER_SIZE];
390                        int bytesRead;
391                        while ((bytesRead = inputStream.read(buffer)) != -1) {
392                                this.crc.update(buffer, 0, bytesRead);
393                                this.size += bytesRead;
394                        }
395                }
396
397                public void setupStoredEntry(JarEntry entry) {
398                        entry.setSize(this.size);
399                        entry.setCompressedSize(this.size);
400                        entry.setCrc(this.crc.getValue());
401                        entry.setMethod(ZipEntry.STORED);
402                }
403
404        }
405
406        /**
407         * An {@code EntryTransformer} enables the transformation of {@link JarEntry jar
408         * entries} during the writing process.
409         */
410        interface EntryTransformer {
411
412                JarEntry transform(JarEntry jarEntry);
413
414        }
415
416        /**
417         * An {@code EntryTransformer} that returns the entry unchanged.
418         */
419        private static final class IdentityEntryTransformer implements EntryTransformer {
420
421                @Override
422                public JarEntry transform(JarEntry jarEntry) {
423                        return jarEntry;
424                }
425
426        }
427
428}