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