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      */
quote(String path)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 
SourceProcessor( @onNull IJavaProject javaProject, @NonNull BuildToolInfo buildToolInfo, @NonNull IFolder genFolder, @NonNull DefaultSourceChangeHandler deltaVisitor)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 
SourceProcessor( @onNull IJavaProject javaProject, @NonNull BuildToolInfo buildToolInfo, @NonNull IFolder genFolder)122     protected SourceProcessor(
123             @NonNull IJavaProject javaProject,
124             @NonNull BuildToolInfo buildToolInfo,
125             @NonNull IFolder genFolder) {
126         this(javaProject, buildToolInfo, genFolder, new DefaultSourceChangeHandler());
127     }
128 
setBuildToolInfo(BuildToolInfo buildToolInfo)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      */
isOutput(IFile file)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      */
isDependency(IFile file)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 
addData(SourceFileData data)168     void addData(SourceFileData data) {
169         mFiles.put(data.getSourceFile(), data);
170     }
171 
getFileData(IFile file)172     SourceFileData getFileData(IFile file) {
173         return mFiles.get(file);
174     }
175 
getAllFileData()176     Collection<SourceFileData> getAllFileData() {
177         return mFiles.values();
178     }
179 
getChangeHandler()180     public final DefaultSourceChangeHandler getChangeHandler() {
181         return mDeltaVisitor;
182     }
183 
getJavaProject()184     final IJavaProject getJavaProject() {
185         return mJavaProject;
186     }
187 
getBuildToolInfo()188     final BuildToolInfo getBuildToolInfo() {
189         return mBuildToolInfo;
190     }
191 
getGenFolder()192     final IFolder getGenFolder() {
193         return mGenFolder;
194     }
195 
getToCompile()196     final List<IFile> getToCompile() {
197         return mToCompile;
198     }
199 
getRemovedFile()200     final List<IFile> getRemovedFile() {
201         return mRemoved;
202     }
203 
addFileToCompile(IFile file)204     final void addFileToCompile(IFile file) {
205         mToCompile.add(file);
206     }
207 
prepareFullBuild(IProject project)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 
doneVisiting(IProject project)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      */
getExtensions()235     protected abstract Set<String> getExtensions();
236 
getSavePropertyName()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      */
compileFiles(BaseBuilder builder, IProject project, IAndroidTarget projectTarget, List<IPath> sourceFolders, List<File> libraryProjectsOut, IProgressMonitor monitor)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 
doCompileFiles( List<IFile> filesToCompile, BaseBuilder builder, IProject project, IAndroidTarget projectTarget, List<IPath> sourceFolders, List<IFile> notCompiledOut, List<File> libraryProjectsOut, IProgressMonitor monitor)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      */
setCompilationStatus(int status)308     protected void setCompilationStatus(int status) {
309         mLastCompilationStatus |= status;
310     }
311 
doRemoveFiles(SourceFileData data)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 
loadState(IProject project)321     public final boolean loadState(IProject project) {
322         return ProjectHelper.loadBooleanProperty(project, getSavePropertyName(),
323                 true /*defaultValue*/);
324     }
325 
saveState(IProject project)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 
loadOutputAndDependencies()332     protected abstract void loadOutputAndDependencies();
333 
334 
getSourceFolderFor(IFile file)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      */
buildSourceFileList()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      */
scanFolderForSourceFiles(IFolder sourceFolder, IFolder folder)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      */
mergeFileModifications(DefaultSourceChangeHandler visitor)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