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.archive; 018 019import java.io.File; 020import java.io.FileInputStream; 021import java.io.IOException; 022import java.net.MalformedURLException; 023import java.net.URL; 024import java.util.ArrayList; 025import java.util.Arrays; 026import java.util.Collections; 027import java.util.Comparator; 028import java.util.Deque; 029import java.util.HashSet; 030import java.util.Iterator; 031import java.util.LinkedList; 032import java.util.List; 033import java.util.NoSuchElementException; 034import java.util.Set; 035import java.util.jar.Manifest; 036 037/** 038 * {@link Archive} implementation backed by an exploded archive directory. 039 * 040 * @author Phillip Webb 041 * @author Andy Wilkinson 042 */ 043public class ExplodedArchive implements Archive { 044 045 private static final Set<String> SKIPPED_NAMES = new HashSet<String>( 046 Arrays.asList(".", "..")); 047 048 private final File root; 049 050 private final boolean recursive; 051 052 private File manifestFile; 053 054 private Manifest manifest; 055 056 /** 057 * Create a new {@link ExplodedArchive} instance. 058 * @param root the root folder 059 */ 060 public ExplodedArchive(File root) { 061 this(root, true); 062 } 063 064 /** 065 * Create a new {@link ExplodedArchive} instance. 066 * @param root the root folder 067 * @param recursive if recursive searching should be used to locate the manifest. 068 * Defaults to {@code true}, folders with a large tree might want to set this to 069 * {@code 070 * false}. 071 */ 072 public ExplodedArchive(File root, boolean recursive) { 073 if (!root.exists() || !root.isDirectory()) { 074 throw new IllegalArgumentException("Invalid source folder " + root); 075 } 076 this.root = root; 077 this.recursive = recursive; 078 this.manifestFile = getManifestFile(root); 079 } 080 081 private File getManifestFile(File root) { 082 File metaInf = new File(root, "META-INF"); 083 return new File(metaInf, "MANIFEST.MF"); 084 } 085 086 @Override 087 public URL getUrl() throws MalformedURLException { 088 return this.root.toURI().toURL(); 089 } 090 091 @Override 092 public Manifest getManifest() throws IOException { 093 if (this.manifest == null && this.manifestFile.exists()) { 094 FileInputStream inputStream = new FileInputStream(this.manifestFile); 095 try { 096 this.manifest = new Manifest(inputStream); 097 } 098 finally { 099 inputStream.close(); 100 } 101 } 102 return this.manifest; 103 } 104 105 @Override 106 public List<Archive> getNestedArchives(EntryFilter filter) throws IOException { 107 List<Archive> nestedArchives = new ArrayList<Archive>(); 108 for (Entry entry : this) { 109 if (filter.matches(entry)) { 110 nestedArchives.add(getNestedArchive(entry)); 111 } 112 } 113 return Collections.unmodifiableList(nestedArchives); 114 } 115 116 @Override 117 public Iterator<Entry> iterator() { 118 return new FileEntryIterator(this.root, this.recursive); 119 } 120 121 protected Archive getNestedArchive(Entry entry) throws IOException { 122 File file = ((FileEntry) entry).getFile(); 123 return (file.isDirectory() ? new ExplodedArchive(file) 124 : new JarFileArchive(file)); 125 } 126 127 @Override 128 public String toString() { 129 try { 130 return getUrl().toString(); 131 } 132 catch (Exception ex) { 133 return "exploded archive"; 134 } 135 } 136 137 /** 138 * File based {@link Entry} {@link Iterator}. 139 */ 140 private static class FileEntryIterator implements Iterator<Entry> { 141 142 private final Comparator<File> entryComparator = new EntryComparator(); 143 144 private final File root; 145 146 private final boolean recursive; 147 148 private final Deque<Iterator<File>> stack = new LinkedList<Iterator<File>>(); 149 150 private File current; 151 152 FileEntryIterator(File root, boolean recursive) { 153 this.root = root; 154 this.recursive = recursive; 155 this.stack.add(listFiles(root)); 156 this.current = poll(); 157 } 158 159 @Override 160 public boolean hasNext() { 161 return this.current != null; 162 } 163 164 @Override 165 public Entry next() { 166 if (this.current == null) { 167 throw new NoSuchElementException(); 168 } 169 File file = this.current; 170 if (file.isDirectory() 171 && (this.recursive || file.getParentFile().equals(this.root))) { 172 this.stack.addFirst(listFiles(file)); 173 } 174 this.current = poll(); 175 String name = file.toURI().getPath() 176 .substring(this.root.toURI().getPath().length()); 177 return new FileEntry(name, file); 178 } 179 180 private Iterator<File> listFiles(File file) { 181 File[] files = file.listFiles(); 182 if (files == null) { 183 return Collections.<File>emptyList().iterator(); 184 } 185 Arrays.sort(files, this.entryComparator); 186 return Arrays.asList(files).iterator(); 187 } 188 189 private File poll() { 190 while (!this.stack.isEmpty()) { 191 while (this.stack.peek().hasNext()) { 192 File file = this.stack.peek().next(); 193 if (!SKIPPED_NAMES.contains(file.getName())) { 194 return file; 195 } 196 } 197 this.stack.poll(); 198 } 199 return null; 200 } 201 202 @Override 203 public void remove() { 204 throw new UnsupportedOperationException("remove"); 205 } 206 207 /** 208 * {@link Comparator} that orders {@link File} entries by their absolute paths. 209 */ 210 private static class EntryComparator implements Comparator<File> { 211 212 @Override 213 public int compare(File o1, File o2) { 214 return o1.getAbsolutePath().compareTo(o2.getAbsolutePath()); 215 } 216 217 } 218 219 } 220 221 /** 222 * {@link Entry} backed by a File. 223 */ 224 private static class FileEntry implements Entry { 225 226 private final String name; 227 228 private final File file; 229 230 FileEntry(String name, File file) { 231 this.name = name; 232 this.file = file; 233 } 234 235 public File getFile() { 236 return this.file; 237 } 238 239 @Override 240 public boolean isDirectory() { 241 return this.file.isDirectory(); 242 } 243 244 @Override 245 public String getName() { 246 return this.name; 247 } 248 249 } 250 251}