1 package org.robolectric.res; 2 3 import static java.util.Arrays.asList; 4 5 import java.io.BufferedInputStream; 6 import java.io.File; 7 import java.io.IOException; 8 import java.io.InputStream; 9 import java.net.MalformedURLException; 10 import java.net.URI; 11 import java.net.URL; 12 import java.util.ArrayList; 13 import java.util.Enumeration; 14 import java.util.LinkedHashMap; 15 import java.util.List; 16 import java.util.Map; 17 import java.util.NavigableMap; 18 import java.util.NavigableSet; 19 import java.util.TreeMap; 20 import java.util.jar.JarEntry; 21 import java.util.jar.JarFile; 22 import org.robolectric.util.Join; 23 import org.robolectric.util.Util; 24 25 abstract public class Fs { fromJar(URL url)26 public static Fs fromJar(URL url) { 27 return new JarFs(new File(fixFileURL(url).getPath())); 28 } 29 fixFileURL(URL u)30 private static URI fixFileURL(URL u) { 31 if (!"file".equals(u.getProtocol())) { 32 throw new IllegalArgumentException(); 33 } 34 return new File(u.getPath()).toURI(); 35 } 36 37 /** 38 * @deprecated Use {@link #fromURL(URL)} instead. 39 */ 40 @Deprecated fileFromPath(String urlString)41 public static FsFile fileFromPath(String urlString) { 42 if (urlString.startsWith("jar:")) { 43 String[] parts = urlString.replaceFirst("jar:", "").split("!", 0); 44 Fs fs = new JarFs(new File(parts[0])); 45 return fs.join(parts[1].substring(1)); 46 } else { 47 return new FileFsFile(new File(urlString)); 48 } 49 } 50 fromURL(URL url)51 public static FsFile fromURL(URL url) { 52 switch (url.getProtocol()) { 53 case "file": 54 return new FileFsFile(new File(url.getPath())); 55 case "jar": 56 String[] parts = url.getPath().split("!", 0); 57 try { 58 Fs fs = fromJar(new URL(parts[0])); 59 return fs.join(parts[1].substring(1)); 60 } catch (MalformedURLException e) { 61 throw new IllegalArgumentException(e); 62 } 63 default: 64 throw new IllegalArgumentException("unsupported fs type for '" + url + "'"); 65 } 66 } 67 newFile(File file)68 public static FsFile newFile(File file) { 69 return new FileFsFile(file); 70 } 71 newJarFile(File file)72 public static FsFile newJarFile(File file) { 73 JarFs jarFs = new JarFs(file); 74 return jarFs.new JarFsFile(""); 75 } 76 newFile(String filePath)77 public static FsFile newFile(String filePath) { 78 return new FileFsFile(filePath); 79 } 80 currentDirectory()81 public static FsFile currentDirectory() { 82 return newFile(new File(".")); 83 } 84 85 static class JarFs extends Fs { 86 private static final Map<File, NavigableMap<String, JarEntry>> CACHE = 87 new LinkedHashMap<File, NavigableMap<String, JarEntry>>() { 88 @Override 89 protected boolean removeEldestEntry(Map.Entry<File, NavigableMap<String, JarEntry>> fileNavigableMapEntry) { 90 return size() > 10; 91 } 92 }; 93 94 private final JarFile jarFile; 95 private final NavigableMap<String, JarEntry> jarEntryMap; 96 JarFs(File file)97 public JarFs(File file) { 98 try { 99 jarFile = new JarFile(file); 100 } catch (IOException e) { 101 throw new RuntimeException(e); 102 } 103 104 NavigableMap<String, JarEntry> cachedMap; 105 synchronized (CACHE) { 106 cachedMap = CACHE.get(file.getAbsoluteFile()); 107 } 108 109 if (cachedMap == null) { 110 cachedMap = new TreeMap<>(); 111 Enumeration<JarEntry> entries = jarFile.entries(); 112 while (entries.hasMoreElements()) { 113 JarEntry jarEntry = entries.nextElement(); 114 cachedMap.put(jarEntry.getName(), jarEntry); 115 } 116 synchronized (CACHE) { 117 CACHE.put(file.getAbsoluteFile(), cachedMap); 118 } 119 } 120 121 jarEntryMap = cachedMap; 122 } 123 join(String folderBaseName)124 @Override public FsFile join(String folderBaseName) { 125 return new JarFsFile(folderBaseName); 126 } 127 128 class JarFsFile implements FsFile { 129 private final String path; 130 JarFsFile(String path)131 public JarFsFile(String path) { 132 this.path = path.replaceAll("^/+", ""); 133 } 134 exists()135 @Override public boolean exists() { 136 return isFile() || isDirectory(); 137 } 138 isDirectory()139 @Override public boolean isDirectory() { 140 return jarEntryMap.containsKey(path + "/"); 141 } 142 isFile()143 @Override public boolean isFile() { 144 return jarEntryMap.containsKey(path); 145 } 146 listFiles()147 @Override public FsFile[] listFiles() { 148 return listFiles(fsFile -> true); 149 } 150 listFiles(Filter filter)151 @Override public FsFile[] listFiles(Filter filter) { 152 NavigableSet<String> strings = jarEntryMap.navigableKeySet(); 153 int startOfFilename = 0; 154 155 if (!path.equals("")) { 156 if (!isDirectory()) { 157 return null; 158 } 159 160 strings = strings.subSet(path + "/", false, path + "0", false); 161 startOfFilename = path.length() + 2; 162 } 163 164 List<FsFile> fsFiles = new ArrayList<>(); 165 for (String string : strings) { 166 int nextSlash = string.indexOf('/', startOfFilename); 167 FsFile fsFile; 168 if (nextSlash == string.length() - 1) { 169 // directory entry 170 fsFile = new JarFsFile(string.substring(0, string.length() - 1)); 171 } else if (nextSlash == -1) { 172 // file entry 173 fsFile = new JarFsFile(string); 174 } else { 175 // file within a nested directory, ignore 176 fsFile = null; 177 } 178 179 if (fsFile != null && filter.accept(fsFile)) { 180 fsFiles.add(fsFile); 181 } 182 } 183 return fsFiles.toArray(new FsFile[fsFiles.size()]); 184 } 185 listFileNames()186 @Override public String[] listFileNames() { 187 List<String> fileNames = new ArrayList<>(); 188 for (FsFile fsFile : listFiles()) { 189 fileNames.add(fsFile.getName()); 190 } 191 return fileNames.toArray(new String[fileNames.size()]); 192 } 193 getParent()194 @Override public FsFile getParent() { 195 int index = path.lastIndexOf('/'); 196 String parent = index != -1 ? path.substring(0, index) : ""; 197 return new JarFsFile(parent); 198 } 199 getName()200 @Override public String getName() { 201 int index = path.lastIndexOf('/'); 202 return index != -1 ? path.substring(index + 1, path.length()) : path; 203 } 204 getInputStream()205 @Override public InputStream getInputStream() throws IOException { 206 return new BufferedInputStream(jarFile.getInputStream(jarEntryMap.get(path))); 207 } 208 getBytes()209 @Override public byte[] getBytes() throws IOException { 210 return Util.readBytes(jarFile.getInputStream(jarEntryMap.get(path))); 211 } 212 join(String... pathParts)213 @Override public FsFile join(String... pathParts) { 214 return new JarFsFile(path + "/" + Join.join("/", asList(pathParts))); 215 } 216 getBaseName()217 @Override public String getBaseName() { 218 String name = getName(); 219 int dotIndex = name.indexOf("."); 220 return dotIndex >= 0 ? name.substring(0, dotIndex) : name; 221 } 222 getPath()223 @Override public String getPath() { 224 return "jar:file:" + getJarFileName() + "!/" + path; 225 } 226 227 @Override length()228 public long length() { 229 return jarFile.getEntry(path).getSize(); 230 } 231 232 @Override equals(Object o)233 public boolean equals(Object o) { 234 if (this == o) return true; 235 if (o == null || getClass() != o.getClass()) return false; 236 237 JarFsFile jarFsFile = (JarFsFile) o; 238 239 if (!getJarFileName().equals(jarFsFile.getJarFileName())) return false; 240 if (!path.equals(jarFsFile.path)) return false; 241 242 return true; 243 } 244 getJarFileName()245 private String getJarFileName() { 246 return jarFile.getName(); 247 } 248 249 @Override hashCode()250 public int hashCode() { 251 return getJarFileName().hashCode() * 31 + path.hashCode(); 252 } 253 toString()254 @Override public String toString() { 255 return getPath(); 256 } 257 } 258 } 259 join(String folderBaseName)260 abstract public FsFile join(String folderBaseName); 261 } 262