Home | History | Annotate | Download | only in direct
      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