/* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.dx.cf.direct; import com.android.dx.util.FileUtils; import java.io.File; import java.io.IOException; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.util.zip.ZipFile; import java.util.zip.ZipEntry; import java.util.Arrays; import java.util.Comparator; import java.util.ArrayList; import java.util.Collections; /** * Opens all the class files found in a class path element. Path elements * can point to class files, {jar,zip,apk} files, or directories containing * class files. */ public class ClassPathOpener { /** {@code non-null;} pathname to start with */ private final String pathname; /** {@code non-null;} callback interface */ private final Consumer consumer; /** * If true, sort such that classes appear before their inner * classes and "package-info" occurs before all other classes in that * package. */ private final boolean sort; /** * Callback interface for {@code ClassOpener}. */ public interface Consumer { /** * Provides the file name and byte array for a class path element. * * @param name {@code non-null;} filename of element. May not be a valid * filesystem path. * * @param bytes {@code non-null;} file data * @return true on success. Result is or'd with all other results * from {@code processFileBytes} and returned to the caller * of {@code process()}. */ boolean processFileBytes(String name, byte[] bytes); /** * Informs consumer that an exception occurred while processing * this path element. Processing will continue if possible. * * @param ex {@code non-null;} exception */ void onException(Exception ex); /** * Informs consumer that processing of an archive file has begun. * * @param file {@code non-null;} archive file being processed */ void onProcessArchiveStart(File file); } /** * Constructs an instance. * * @param pathname {@code non-null;} path element to process * @param sort if true, sort such that classes appear before their inner * classes and "package-info" occurs before all other classes in that * package. * @param consumer {@code non-null;} callback interface */ public ClassPathOpener(String pathname, boolean sort, Consumer consumer) { this.pathname = pathname; this.sort = sort; this.consumer = consumer; } /** * Processes a path element. * * @return the OR of all return values * from {@code Consumer.processFileBytes()}. */ public boolean process() { File file = new File(pathname); return processOne(file, true); } /** * Processes one file. * * @param file {@code non-null;} the file to process * @param topLevel whether this is a top-level file (that is, * specified directly on the commandline) * @return whether any processing actually happened */ private boolean processOne(File file, boolean topLevel) { try { if (file.isDirectory()) { return processDirectory(file, topLevel); } String path = file.getPath(); if (path.endsWith(".zip") || path.endsWith(".jar") || path.endsWith(".apk")) { return processArchive(file); } byte[] bytes = FileUtils.readFile(file); return consumer.processFileBytes(path, bytes); } catch (Exception ex) { consumer.onException(ex); return false; } } /** * Sorts java class names such that outer classes preceed their inner * classes and "package-info" preceeds all other classes in its package. * * @param a {@code non-null;} first class name * @param b {@code non-null;} second class name * @return {@code compareTo()}-style result */ private static int compareClassNames(String a, String b) { // Ensure inner classes sort second a = a.replace('$','0'); b = b.replace('$','0'); /* * Assuming "package-info" only occurs at the end, ensures package-info * sorts first. */ a = a.replace("package-info", ""); b = b.replace("package-info", ""); return a.compareTo(b); } /** * Processes a directory recursively. * * @param dir {@code non-null;} file representing the directory * @param topLevel whether this is a top-level directory (that is, * specified directly on the commandline) * @return whether any processing actually happened */ private boolean processDirectory(File dir, boolean topLevel) { if (topLevel) { dir = new File(dir, "."); } File[] files = dir.listFiles(); int len = files.length; boolean any = false; if (sort) { Arrays.sort(files, new Comparator<File>() { public int compare(File a, File b) { return compareClassNames(a.getName(), b.getName()); } }); } for (int i = 0; i < len; i++) { any |= processOne(files[i], false); } return any; } /** * Processes the contents of an archive ({@code .zip}, * {@code .jar}, or {@code .apk}). * * @param file {@code non-null;} archive file to process * @return whether any processing actually happened * @throws IOException on i/o problem */ private boolean processArchive(File file) throws IOException { ZipFile zip = new ZipFile(file); ByteArrayOutputStream baos = new ByteArrayOutputStream(40000); byte[] buf = new byte[20000]; boolean any = false; ArrayList<? extends java.util.zip.ZipEntry> entriesList = Collections.list(zip.entries()); if (sort) { Collections.sort(entriesList, new Comparator<ZipEntry>() { public int compare (ZipEntry a, ZipEntry b) { return compareClassNames(a.getName(), b.getName()); } }); } consumer.onProcessArchiveStart(file); for (ZipEntry one : entriesList) { if (one.isDirectory()) { continue; } String path = one.getName(); InputStream in = zip.getInputStream(one); baos.reset(); for (;;) { int amt = in.read(buf); if (amt < 0) { break; } baos.write(buf, 0, amt); } in.close(); byte[] bytes = baos.toByteArray(); any |= consumer.processFileBytes(path, bytes); } zip.close(); return any; } }