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}