1 /* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.dx.cf.direct; 18 19 import com.android.dx.util.FileUtils; 20 21 import java.io.File; 22 import java.io.IOException; 23 import java.io.ByteArrayOutputStream; 24 import java.io.InputStream; 25 import java.util.zip.ZipFile; 26 import java.util.zip.ZipEntry; 27 import java.util.Arrays; 28 import java.util.Comparator; 29 import java.util.ArrayList; 30 import java.util.Collections; 31 32 /** 33 * Opens all the class files found in a class path element. Path elements 34 * can point to class files, {jar,zip,apk} files, or directories containing 35 * class files. 36 */ 37 public class ClassPathOpener { 38 39 /** {@code non-null;} pathname to start with */ 40 private final String pathname; 41 /** {@code non-null;} callback interface */ 42 private final Consumer consumer; 43 /** 44 * If true, sort such that classes appear before their inner 45 * classes and "package-info" occurs before all other classes in that 46 * package. 47 */ 48 private final boolean sort; 49 50 /** 51 * Callback interface for {@code ClassOpener}. 52 */ 53 public interface Consumer { 54 55 /** 56 * Provides the file name and byte array for a class path element. 57 * 58 * @param name {@code non-null;} filename of element. May not be a valid 59 * filesystem path. 60 * 61 * @param lastModified milliseconds since 1970-Jan-1 00:00:00 GMT 62 * @param bytes {@code non-null;} file data 63 * @return true on success. Result is or'd with all other results 64 * from {@code processFileBytes} and returned to the caller 65 * of {@code process()}. 66 */ 67 boolean processFileBytes(String name, long lastModified, byte[] bytes); 68 69 /** 70 * Informs consumer that an exception occurred while processing 71 * this path element. Processing will continue if possible. 72 * 73 * @param ex {@code non-null;} exception 74 */ 75 void onException(Exception ex); 76 77 /** 78 * Informs consumer that processing of an archive file has begun. 79 * 80 * @param file {@code non-null;} archive file being processed 81 */ 82 void onProcessArchiveStart(File file); 83 } 84 85 /** 86 * Constructs an instance. 87 * 88 * @param pathname {@code non-null;} path element to process 89 * @param sort if true, sort such that classes appear before their inner 90 * classes and "package-info" occurs before all other classes in that 91 * package. 92 * @param consumer {@code non-null;} callback interface 93 */ 94 public ClassPathOpener(String pathname, boolean sort, Consumer consumer) { 95 this.pathname = pathname; 96 this.sort = sort; 97 this.consumer = consumer; 98 } 99 100 /** 101 * Processes a path element. 102 * 103 * @return the OR of all return values 104 * from {@code Consumer.processFileBytes()}. 105 */ 106 public boolean process() { 107 File file = new File(pathname); 108 109 return processOne(file, true); 110 } 111 112 /** 113 * Processes one file. 114 * 115 * @param file {@code non-null;} the file to process 116 * @param topLevel whether this is a top-level file (that is, 117 * specified directly on the commandline) 118 * @return whether any processing actually happened 119 */ 120 private boolean processOne(File file, boolean topLevel) { 121 try { 122 if (file.isDirectory()) { 123 return processDirectory(file, topLevel); 124 } 125 126 String path = file.getPath(); 127 128 if (path.endsWith(".zip") || 129 path.endsWith(".jar") || 130 path.endsWith(".apk")) { 131 return processArchive(file); 132 } 133 134 byte[] bytes = FileUtils.readFile(file); 135 return consumer.processFileBytes(path, file.lastModified(), bytes); 136 } catch (Exception ex) { 137 consumer.onException(ex); 138 return false; 139 } 140 } 141 142 /** 143 * Sorts java class names such that outer classes preceed their inner 144 * classes and "package-info" preceeds all other classes in its package. 145 * 146 * @param a {@code non-null;} first class name 147 * @param b {@code non-null;} second class name 148 * @return {@code compareTo()}-style result 149 */ 150 private static int compareClassNames(String a, String b) { 151 // Ensure inner classes sort second 152 a = a.replace('$','0'); 153 b = b.replace('$','0'); 154 155 /* 156 * Assuming "package-info" only occurs at the end, ensures package-info 157 * sorts first. 158 */ 159 a = a.replace("package-info", ""); 160 b = b.replace("package-info", ""); 161 162 return a.compareTo(b); 163 } 164 165 /** 166 * Processes a directory recursively. 167 * 168 * @param dir {@code non-null;} file representing the directory 169 * @param topLevel whether this is a top-level directory (that is, 170 * specified directly on the commandline) 171 * @return whether any processing actually happened 172 */ 173 private boolean processDirectory(File dir, boolean topLevel) { 174 if (topLevel) { 175 dir = new File(dir, "."); 176 } 177 178 File[] files = dir.listFiles(); 179 int len = files.length; 180 boolean any = false; 181 182 if (sort) { 183 Arrays.sort(files, new Comparator<File>() { 184 public int compare(File a, File b) { 185 return compareClassNames(a.getName(), b.getName()); 186 } 187 }); 188 } 189 190 for (int i = 0; i < len; i++) { 191 any |= processOne(files[i], false); 192 } 193 194 return any; 195 } 196 197 /** 198 * Processes the contents of an archive ({@code .zip}, 199 * {@code .jar}, or {@code .apk}). 200 * 201 * @param file {@code non-null;} archive file to process 202 * @return whether any processing actually happened 203 * @throws IOException on i/o problem 204 */ 205 private boolean processArchive(File file) throws IOException { 206 ZipFile zip = new ZipFile(file); 207 ByteArrayOutputStream baos = new ByteArrayOutputStream(40000); 208 byte[] buf = new byte[20000]; 209 boolean any = false; 210 211 ArrayList<? extends java.util.zip.ZipEntry> entriesList 212 = Collections.list(zip.entries()); 213 214 if (sort) { 215 Collections.sort(entriesList, new Comparator<ZipEntry>() { 216 public int compare (ZipEntry a, ZipEntry b) { 217 return compareClassNames(a.getName(), b.getName()); 218 } 219 }); 220 } 221 222 consumer.onProcessArchiveStart(file); 223 224 for (ZipEntry one : entriesList) { 225 if (one.isDirectory()) { 226 continue; 227 } 228 229 String path = one.getName(); 230 InputStream in = zip.getInputStream(one); 231 232 baos.reset(); 233 for (;;) { 234 int amt = in.read(buf); 235 if (amt < 0) { 236 break; 237 } 238 239 baos.write(buf, 0, amt); 240 } 241 242 in.close(); 243 244 byte[] bytes = baos.toByteArray(); 245 any |= consumer.processFileBytes(path, one.getTime(), bytes); 246 } 247 248 zip.close(); 249 return any; 250 } 251 } 252