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}