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 bytes {@code non-null;} file data 62 * @return true on success. Result is or'd with all other results 63 * from {@code processFileBytes} and returned to the caller 64 * of {@code process()}. 65 */ processFileBytes(String name, byte[] bytes)66 boolean processFileBytes(String name, byte[] bytes); 67 68 /** 69 * Informs consumer that an exception occurred while processing 70 * this path element. Processing will continue if possible. 71 * 72 * @param ex {@code non-null;} exception 73 */ onException(Exception ex)74 void onException(Exception ex); 75 76 /** 77 * Informs consumer that processing of an archive file has begun. 78 * 79 * @param file {@code non-null;} archive file being processed 80 */ onProcessArchiveStart(File file)81 void onProcessArchiveStart(File file); 82 } 83 84 /** 85 * Constructs an instance. 86 * 87 * @param pathname {@code non-null;} path element to process 88 * @param sort if true, sort such that classes appear before their inner 89 * classes and "package-info" occurs before all other classes in that 90 * package. 91 * @param consumer {@code non-null;} callback interface 92 */ ClassPathOpener(String pathname, boolean sort, Consumer consumer)93 public ClassPathOpener(String pathname, boolean sort, Consumer consumer) { 94 this.pathname = pathname; 95 this.sort = sort; 96 this.consumer = consumer; 97 } 98 99 /** 100 * Processes a path element. 101 * 102 * @return the OR of all return values 103 * from {@code Consumer.processFileBytes()}. 104 */ process()105 public boolean process() { 106 File file = new File(pathname); 107 108 return processOne(file, true); 109 } 110 111 /** 112 * Processes one file. 113 * 114 * @param file {@code non-null;} the file to process 115 * @param topLevel whether this is a top-level file (that is, 116 * specified directly on the commandline) 117 * @return whether any processing actually happened 118 */ processOne(File file, boolean topLevel)119 private boolean processOne(File file, boolean topLevel) { 120 try { 121 if (file.isDirectory()) { 122 return processDirectory(file, topLevel); 123 } 124 125 String path = file.getPath(); 126 127 if (path.endsWith(".zip") || 128 path.endsWith(".jar") || 129 path.endsWith(".apk")) { 130 return processArchive(file); 131 } 132 133 byte[] bytes = FileUtils.readFile(file); 134 return consumer.processFileBytes(path, bytes); 135 } catch (Exception ex) { 136 consumer.onException(ex); 137 return false; 138 } 139 } 140 141 /** 142 * Sorts java class names such that outer classes preceed their inner 143 * classes and "package-info" preceeds all other classes in its package. 144 * 145 * @param a {@code non-null;} first class name 146 * @param b {@code non-null;} second class name 147 * @return {@code compareTo()}-style result 148 */ compareClassNames(String a, String b)149 private static int compareClassNames(String a, String b) { 150 // Ensure inner classes sort second 151 a = a.replace('$','0'); 152 b = b.replace('$','0'); 153 154 /* 155 * Assuming "package-info" only occurs at the end, ensures package-info 156 * sorts first. 157 */ 158 a = a.replace("package-info", ""); 159 b = b.replace("package-info", ""); 160 161 return a.compareTo(b); 162 } 163 164 /** 165 * Processes a directory recursively. 166 * 167 * @param dir {@code non-null;} file representing the directory 168 * @param topLevel whether this is a top-level directory (that is, 169 * specified directly on the commandline) 170 * @return whether any processing actually happened 171 */ processDirectory(File dir, boolean topLevel)172 private boolean processDirectory(File dir, boolean topLevel) { 173 if (topLevel) { 174 dir = new File(dir, "."); 175 } 176 177 File[] files = dir.listFiles(); 178 int len = files.length; 179 boolean any = false; 180 181 if (sort) { 182 Arrays.sort(files, new Comparator<File>() { 183 public int compare(File a, File b) { 184 return compareClassNames(a.getName(), b.getName()); 185 } 186 }); 187 } 188 189 for (int i = 0; i < len; i++) { 190 any |= processOne(files[i], false); 191 } 192 193 return any; 194 } 195 196 /** 197 * Processes the contents of an archive ({@code .zip}, 198 * {@code .jar}, or {@code .apk}). 199 * 200 * @param file {@code non-null;} archive file to process 201 * @return whether any processing actually happened 202 * @throws IOException on i/o problem 203 */ processArchive(File file)204 private boolean processArchive(File file) throws IOException { 205 ZipFile zip = new ZipFile(file); 206 ByteArrayOutputStream baos = new ByteArrayOutputStream(40000); 207 byte[] buf = new byte[20000]; 208 boolean any = false; 209 210 ArrayList<? extends java.util.zip.ZipEntry> entriesList 211 = Collections.list(zip.entries()); 212 213 if (sort) { 214 Collections.sort(entriesList, new Comparator<ZipEntry>() { 215 public int compare (ZipEntry a, ZipEntry b) { 216 return compareClassNames(a.getName(), b.getName()); 217 } 218 }); 219 } 220 221 consumer.onProcessArchiveStart(file); 222 223 for (ZipEntry one : entriesList) { 224 if (one.isDirectory()) { 225 continue; 226 } 227 228 String path = one.getName(); 229 InputStream in = zip.getInputStream(one); 230 231 baos.reset(); 232 for (;;) { 233 int amt = in.read(buf); 234 if (amt < 0) { 235 break; 236 } 237 238 baos.write(buf, 0, amt); 239 } 240 241 in.close(); 242 243 byte[] bytes = baos.toByteArray(); 244 any |= consumer.processFileBytes(path, bytes); 245 } 246 247 zip.close(); 248 return any; 249 } 250 } 251