Home | History | Annotate | Download | only in build
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
      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.ide.eclipse.adt.internal.build;
     18 
     19 import com.android.SdkConstants;
     20 import com.android.annotations.NonNull;
     21 import com.android.ide.eclipse.adt.internal.build.builders.BaseBuilder;
     22 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
     23 import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
     24 import com.android.sdklib.BuildToolInfo;
     25 import com.android.sdklib.IAndroidTarget;
     26 
     27 import org.eclipse.core.resources.IFile;
     28 import org.eclipse.core.resources.IFolder;
     29 import org.eclipse.core.resources.IProject;
     30 import org.eclipse.core.resources.IResource;
     31 import org.eclipse.core.resources.IWorkspaceRoot;
     32 import org.eclipse.core.resources.ResourcesPlugin;
     33 import org.eclipse.core.runtime.CoreException;
     34 import org.eclipse.core.runtime.IPath;
     35 import org.eclipse.core.runtime.IProgressMonitor;
     36 import org.eclipse.jdt.core.IJavaProject;
     37 
     38 import java.io.File;
     39 import java.util.ArrayList;
     40 import java.util.Collection;
     41 import java.util.HashMap;
     42 import java.util.List;
     43 import java.util.Locale;
     44 import java.util.Map;
     45 import java.util.Set;
     46 
     47 /**
     48  * Base class to handle generated java code.
     49  *
     50  * It provides management for modified source file list, deleted source file list, reconciliation
     51  * of previous lists, storing the current state of the build.
     52  *
     53  */
     54 public abstract class SourceProcessor {
     55 
     56     public final static int COMPILE_STATUS_NONE = 0;
     57     public final static int COMPILE_STATUS_CODE = 0x1;
     58     public final static int COMPILE_STATUS_RES = 0x2;
     59 
     60     /** List of all source files, their dependencies, and their output. */
     61     private final Map<IFile, SourceFileData> mFiles = new HashMap<IFile, SourceFileData>();
     62 
     63     private final IJavaProject mJavaProject;
     64     private BuildToolInfo mBuildToolInfo;
     65     private final IFolder mGenFolder;
     66     private final SourceChangeHandler mDeltaVisitor;
     67 
     68     /** List of source files pending compilation at the next build */
     69     private final List<IFile> mToCompile = new ArrayList<IFile>();
     70 
     71     /** List of removed source files pending cleaning at the next build. */
     72     private final List<IFile> mRemoved = new ArrayList<IFile>();
     73 
     74     private int mLastCompilationStatus = COMPILE_STATUS_NONE;
     75 
     76     /**
     77      * Quotes a path inside "". If the platform is not windows, the path is returned as is.
     78      * @param path the path to quote
     79      * @return the quoted path.
     80      */
     81     public static String quote(String path) {
     82         if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) {
     83             if (path.endsWith(File.separator)) {
     84                 path = path.substring(0, path.length() -1);
     85             }
     86             return "\"" + path + "\"";
     87         }
     88 
     89         return path;
     90     }
     91 
     92     protected SourceProcessor(@NonNull IJavaProject javaProject,
     93             @NonNull BuildToolInfo buildToolInfo, @NonNull IFolder genFolder,
     94             @NonNull SourceChangeHandler deltaVisitor) {
     95         mJavaProject = javaProject;
     96         mBuildToolInfo = buildToolInfo;
     97         mGenFolder = genFolder;
     98         mDeltaVisitor = deltaVisitor;
     99 
    100         mDeltaVisitor.init(this);
    101 
    102         IProject project = javaProject.getProject();
    103 
    104         // get all the source files
    105         buildSourceFileList();
    106 
    107         // load the known dependencies
    108         loadOutputAndDependencies();
    109 
    110         boolean mustCompile = loadState(project);
    111 
    112         // if we stored that we have to compile some files, we build the list that will compile them
    113         // all. For now we have to reuse the full list since we don't know which files needed
    114         // compilation.
    115         if (mustCompile) {
    116             mToCompile.addAll(mFiles.keySet());
    117         }
    118     }
    119 
    120     protected SourceProcessor(@NonNull IJavaProject javaProject,
    121             @NonNull BuildToolInfo buildToolInfo, @NonNull IFolder genFolder) {
    122         this(javaProject, buildToolInfo, genFolder, new SourceChangeHandler());
    123     }
    124 
    125     public void setBuildToolInfo(BuildToolInfo buildToolInfo) {
    126         mBuildToolInfo = buildToolInfo;
    127     }
    128 
    129 
    130     /**
    131      * Returns whether the given file is an output of this processor by return the source
    132      * file that generated it.
    133      * @param file the file to test.
    134      * @return the source file that generated the given file or null.
    135      */
    136     IFile isOutput(IFile file) {
    137         for (SourceFileData data : mFiles.values()) {
    138             if (data.generated(file)) {
    139                 return data.getSourceFile();
    140             }
    141         }
    142 
    143         return null;
    144     }
    145 
    146     /**
    147      * Returns whether the given file is a dependency for other files by returning a list
    148      * of file depending on the given file.
    149      * @param file the file to test.
    150      * @return a list of files that depend on the given file or an empty list if there
    151      *    are no matches.
    152      */
    153     List<IFile> isDependency(IFile file) {
    154         ArrayList<IFile> files = new ArrayList<IFile>();
    155         for (SourceFileData data : mFiles.values()) {
    156             if (data.dependsOn(file)) {
    157                 files.add(data.getSourceFile());
    158             }
    159         }
    160 
    161         return files;
    162     }
    163 
    164     void addData(SourceFileData data) {
    165         mFiles.put(data.getSourceFile(), data);
    166     }
    167 
    168     SourceFileData getFileData(IFile file) {
    169         return mFiles.get(file);
    170     }
    171 
    172     Collection<SourceFileData> getAllFileData() {
    173         return mFiles.values();
    174     }
    175 
    176     public final SourceChangeHandler getChangeHandler() {
    177         return mDeltaVisitor;
    178     }
    179 
    180     final IJavaProject getJavaProject() {
    181         return mJavaProject;
    182     }
    183 
    184     final BuildToolInfo getBuildToolInfo() {
    185         return mBuildToolInfo;
    186     }
    187 
    188     final IFolder getGenFolder() {
    189         return mGenFolder;
    190     }
    191 
    192     final List<IFile> getToCompile() {
    193         return mToCompile;
    194     }
    195 
    196     final List<IFile> getRemovedFile() {
    197         return mRemoved;
    198     }
    199 
    200     final void addFileToCompile(IFile file) {
    201         mToCompile.add(file);
    202     }
    203 
    204     public final void prepareFullBuild(IProject project) {
    205         mDeltaVisitor.reset();
    206 
    207         mToCompile.clear();
    208         mRemoved.clear();
    209 
    210         // get all the source files
    211         buildSourceFileList();
    212 
    213         mToCompile.addAll(mFiles.keySet());
    214 
    215         saveState(project);
    216     }
    217 
    218     public final void doneVisiting(IProject project) {
    219         // merge the previous file modification lists and the new one.
    220         mergeFileModifications(mDeltaVisitor);
    221 
    222         mDeltaVisitor.reset();
    223 
    224         saveState(project);
    225     }
    226 
    227     /**
    228      * Returns the extension of the source files handled by this processor.
    229      * @return
    230      */
    231     protected abstract Set<String> getExtensions();
    232 
    233     protected abstract String getSavePropertyName();
    234 
    235     /**
    236      * Compiles the source files and return a status bitmask of the type of file that was generated.
    237      *
    238      */
    239     public final int compileFiles(BaseBuilder builder,
    240             IProject project, IAndroidTarget projectTarget,
    241             List<IPath> sourceFolders, List<File> libraryProjectsOut, IProgressMonitor monitor)
    242             throws CoreException {
    243 
    244         mLastCompilationStatus = COMPILE_STATUS_NONE;
    245 
    246         if (mToCompile.size() == 0 && mRemoved.size() == 0) {
    247             return mLastCompilationStatus;
    248         }
    249 
    250         // if a source file is being removed before we managed to compile it, it'll be in
    251         // both list. We *need* to remove it from the compile list or it'll never go away.
    252         for (IFile sourceFile : mRemoved) {
    253             int pos = mToCompile.indexOf(sourceFile);
    254             if (pos != -1) {
    255                 mToCompile.remove(pos);
    256             }
    257         }
    258 
    259         // list of files that have failed compilation.
    260         List<IFile> stillNeedCompilation = new ArrayList<IFile>();
    261 
    262         doCompileFiles(mToCompile, builder, project, projectTarget, sourceFolders,
    263                 stillNeedCompilation, libraryProjectsOut, monitor);
    264 
    265         mToCompile.clear();
    266         mToCompile.addAll(stillNeedCompilation);
    267 
    268         // Remove the files created from source files that have been removed.
    269         for (IFile sourceFile : mRemoved) {
    270             // look if we already know the output
    271             SourceFileData data = getFileData(sourceFile);
    272             if (data != null) {
    273                 doRemoveFiles(data);
    274             }
    275         }
    276 
    277         // remove the associated file data.
    278         for (IFile removedFile : mRemoved) {
    279             mFiles.remove(removedFile);
    280         }
    281 
    282         mRemoved.clear();
    283 
    284         // store the build state. If there are any files that failed to compile, we will
    285         // force a full aidl compile on the next project open. (unless a full compilation succeed
    286         // before the project is closed/re-opened.)
    287         saveState(project);
    288 
    289         return mLastCompilationStatus;
    290     }
    291 
    292     protected abstract void doCompileFiles(
    293             List<IFile> filesToCompile, BaseBuilder builder,
    294             IProject project, IAndroidTarget projectTarget,
    295             List<IPath> sourceFolders, List<IFile> notCompiledOut,
    296             List<File> libraryProjectsOut, IProgressMonitor monitor) throws CoreException;
    297 
    298     /**
    299      * Adds a compilation status. It can be any of (in combination too):
    300      * <p/>
    301      * {@link #COMPILE_STATUS_CODE} means this processor created source code files.
    302      * {@link #COMPILE_STATUS_RES} means this process created resources.
    303      */
    304     protected void setCompilationStatus(int status) {
    305         mLastCompilationStatus |= status;
    306     }
    307 
    308     protected void doRemoveFiles(SourceFileData data) throws CoreException {
    309         List<IFile> outputFiles = data.getOutputFiles();
    310         for (IFile outputFile : outputFiles) {
    311             if (outputFile.exists()) {
    312                 outputFile.getLocation().toFile().delete();
    313             }
    314         }
    315     }
    316 
    317     public final boolean loadState(IProject project) {
    318         return ProjectHelper.loadBooleanProperty(project, getSavePropertyName(),
    319                 true /*defaultValue*/);
    320     }
    321 
    322     public final void saveState(IProject project) {
    323         // TODO: Optimize by saving only the files that need compilation
    324         ProjectHelper.saveStringProperty(project, getSavePropertyName(),
    325                 Boolean.toString(mToCompile.size() > 0));
    326     }
    327 
    328     protected abstract void loadOutputAndDependencies();
    329 
    330 
    331     protected IPath getSourceFolderFor(IFile file) {
    332         // find the source folder for the class so that we can infer the package from the
    333         // difference between the file and its source folder.
    334         List<IPath> sourceFolders = BaseProjectHelper.getSourceClasspaths(getJavaProject());
    335         IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
    336 
    337         for (IPath sourceFolderPath : sourceFolders) {
    338             IFolder sourceFolder = root.getFolder(sourceFolderPath);
    339             // we don't look in the 'gen' source folder as there will be no source in there.
    340             if (sourceFolder.exists() && sourceFolder.equals(getGenFolder()) == false) {
    341                 // look for the source file parent, until we find this source folder.
    342                 IResource parent = file;
    343                 while ((parent = parent.getParent()) != null) {
    344                     if (parent.equals(sourceFolder)) {
    345                         return sourceFolderPath;
    346                     }
    347                 }
    348             }
    349         }
    350 
    351         return null;
    352     }
    353 
    354     /**
    355      * Goes through the build paths and fills the list of files to compile.
    356      *
    357      * @param project The project.
    358      * @param sourceFolderPathList The list of source folder paths.
    359      */
    360     private final void buildSourceFileList() {
    361         mFiles.clear();
    362 
    363         IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
    364         List<IPath> sourceFolderPathList = BaseProjectHelper.getSourceClasspaths(mJavaProject);
    365 
    366         for (IPath sourceFolderPath : sourceFolderPathList) {
    367             IFolder sourceFolder = root.getFolder(sourceFolderPath);
    368             // we don't look in the 'gen' source folder as there will be no source in there.
    369             if (sourceFolder.exists() && sourceFolder.equals(getGenFolder()) == false) {
    370                 scanFolderForSourceFiles(sourceFolder, sourceFolder);
    371             }
    372         }
    373     }
    374 
    375     /**
    376      * Scans a folder and fills the list of files to compile.
    377      * @param sourceFolder the root source folder.
    378      * @param folder The folder to scan.
    379      */
    380     private void scanFolderForSourceFiles(IFolder sourceFolder, IFolder folder) {
    381         try {
    382             IResource[] members = folder.members();
    383             for (IResource r : members) {
    384                 // get the type of the resource
    385                switch (r.getType()) {
    386                    case IResource.FILE: {
    387                        // if this a file, check that the file actually exist
    388                        // and that it's the type of of file that's used in this processor
    389                        String extension = r.exists() ? r.getFileExtension() : null;
    390                        if (extension != null &&
    391                                getExtensions().contains(extension.toLowerCase(Locale.US))) {
    392                            mFiles.put((IFile) r, new SourceFileData((IFile) r));
    393                        }
    394                        break;
    395                    }
    396                    case IResource.FOLDER:
    397                        // recursively go through children
    398                        scanFolderForSourceFiles(sourceFolder, (IFolder)r);
    399                        break;
    400                    default:
    401                        // this would mean it's a project or the workspace root
    402                        // which is unlikely to happen. we do nothing
    403                        break;
    404                }
    405             }
    406         } catch (CoreException e) {
    407             // Couldn't get the members list for some reason. Just return.
    408         }
    409     }
    410 
    411 
    412     /**
    413      * Merge the current list of source file to compile/remove with the one coming from the
    414      * delta visitor
    415      * @param visitor the delta visitor.
    416      */
    417     private void mergeFileModifications(SourceChangeHandler visitor) {
    418         Set<IFile> toRemove = visitor.getRemovedFiles();
    419         Set<IFile> toCompile = visitor.getFilesToCompile();
    420 
    421         // loop through the new toRemove list, and add it to the old one,
    422         // plus remove any file that was still to compile and that are now
    423         // removed
    424         for (IFile r : toRemove) {
    425             if (mRemoved.indexOf(r) == -1) {
    426                 mRemoved.add(r);
    427             }
    428 
    429             int index = mToCompile.indexOf(r);
    430             if (index != -1) {
    431                 mToCompile.remove(index);
    432             }
    433         }
    434 
    435         // now loop through the new files to compile and add it to the list.
    436         // Also look for them in the remove list, this would mean that they
    437         // were removed, then added back, and we shouldn't remove them, just
    438         // recompile them.
    439         for (IFile r : toCompile) {
    440             if (mToCompile.indexOf(r) == -1) {
    441                 mToCompile.add(r);
    442             }
    443 
    444             int index = mRemoved.indexOf(r);
    445             if (index != -1) {
    446                 mRemoved.remove(index);
    447             }
    448         }
    449     }
    450 }
    451