• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2007 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.ide.eclipse.adt.AdtConstants;
20 import com.android.ide.eclipse.adt.AdtPlugin;
21 import com.android.ide.eclipse.adt.AndroidConstants;
22 import com.android.ide.eclipse.adt.internal.build.BaseBuilder.BaseDeltaVisitor;
23 import com.android.ide.eclipse.adt.internal.build.PreCompilerBuilder.AidlData;
24 import com.android.ide.eclipse.adt.internal.project.AndroidManifestParser;
25 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
26 import com.android.sdklib.SdkConstants;
27 
28 import org.eclipse.core.resources.IContainer;
29 import org.eclipse.core.resources.IFile;
30 import org.eclipse.core.resources.IFolder;
31 import org.eclipse.core.resources.IResource;
32 import org.eclipse.core.resources.IResourceDelta;
33 import org.eclipse.core.resources.IResourceDeltaVisitor;
34 import org.eclipse.core.resources.IWorkspaceRoot;
35 import org.eclipse.core.resources.ResourcesPlugin;
36 import org.eclipse.core.runtime.CoreException;
37 import org.eclipse.core.runtime.IPath;
38 
39 import java.util.ArrayList;
40 
41 /**
42  * Resource Delta visitor for the pre-compiler.
43  * <p/>This delta visitor only cares about files that are the source or the result of actions of the
44  * {@link PreCompilerBuilder}:
45  * <ul><li>R.java/Manifest.java generated by compiling the resources</li>
46  * <li>Any Java files generated by <code>aidl</code></li></ul>.
47  *
48  * Therefore it looks for the following:
49  * <ul><li>Any modification in the resource folder</li>
50  * <li>Removed files from the source folder receiving generated Java files</li>
51  * <li>Any modification to aidl files.</li>
52  *
53  */
54 class PreCompilerDeltaVisitor extends BaseDeltaVisitor implements
55         IResourceDeltaVisitor {
56 
57     private enum AidlType {
58         UNKNOWN, INTERFACE, PARCELABLE;
59     }
60 
61     // See comment in #getAidlType()
62 //    private final static Pattern sParcelablePattern = Pattern.compile(
63 //            "^\\s*parcelable\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*;\\s*$");
64 //
65 //    private final static Pattern sInterfacePattern = Pattern.compile(
66 //            "^\\s*interface\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(?:\\{.*)?$");
67 
68     // Result fields.
69     /**
70      * Compile flag. This is set to true if one of the changed/added/removed
71      * file is a resource file. Upon visiting all the delta resources, if
72      * this flag is true, then we know we'll have to compile the resources
73      * into R.java
74      */
75     private boolean mCompileResources = false;
76 
77     /**
78      * Aidl force recompilation flag. If true, we'll attempt to recompile all aidl files.
79      */
80     private boolean mForceAidlCompile = false;
81 
82     /** List of .aidl files found that are modified or new. */
83     private final ArrayList<AidlData> mAidlToCompile = new ArrayList<AidlData>();
84 
85     /** List of .aidl files that have been removed. */
86     private final ArrayList<AidlData> mAidlToRemove = new ArrayList<AidlData>();
87 
88     /** Manifest check/parsing flag. */
89     private boolean mCheckedManifestXml = false;
90 
91     /** Application Package, gathered from the parsing of the manifest */
92     private String mJavaPackage = null;
93     /** minSDKVersion attribute value, gathered from the parsing of the manifest */
94     private String mMinSdkVersion = null;
95 
96     // Internal usage fields.
97     /**
98      * In Resource folder flag. This allows us to know if we're in the
99      * resource folder.
100      */
101     private boolean mInRes = false;
102 
103     /**
104      * Current Source folder. This allows us to know if we're in a source
105      * folder, and which folder.
106      */
107     private IFolder mSourceFolder = null;
108 
109     /** List of source folders. */
110     private ArrayList<IPath> mSourceFolders;
111     private boolean mIsGenSourceFolder = false;
112 
113     private IWorkspaceRoot mRoot;
114 
115 
PreCompilerDeltaVisitor(BaseBuilder builder, ArrayList<IPath> sourceFolders)116     public PreCompilerDeltaVisitor(BaseBuilder builder, ArrayList<IPath> sourceFolders) {
117         super(builder);
118         mSourceFolders = sourceFolders;
119         mRoot = ResourcesPlugin.getWorkspace().getRoot();
120     }
121 
getCompileResources()122     public boolean getCompileResources() {
123         return mCompileResources;
124     }
125 
getForceAidlCompile()126     public boolean getForceAidlCompile() {
127         return mForceAidlCompile;
128     }
129 
getAidlToCompile()130     public ArrayList<AidlData> getAidlToCompile() {
131         return mAidlToCompile;
132     }
133 
getAidlToRemove()134     public ArrayList<AidlData> getAidlToRemove() {
135         return mAidlToRemove;
136     }
137 
138     /**
139      * Returns whether the manifest file was parsed/checked for error during the resource delta
140      * visiting.
141      */
getCheckedManifestXml()142     public boolean getCheckedManifestXml() {
143         return mCheckedManifestXml;
144     }
145 
146     /**
147      * Returns the manifest package if the manifest was checked/parsed.
148      * <p/>
149      * This can return null in two cases:
150      * <ul>
151      * <li>The manifest was not part of the resource change delta, and the manifest was
152      * not checked/parsed ({@link #getCheckedManifestXml()} returns <code>false</code>)</li>
153      * <li>The manifest was parsed ({@link #getCheckedManifestXml()} returns <code>true</code>),
154      * but the package declaration is missing</li>
155      * </ul>
156      * @return the manifest package or null.
157      */
getManifestPackage()158     public String getManifestPackage() {
159         return mJavaPackage;
160     }
161 
162     /**
163      * Returns the minSDkVersion attribute from the manifest if it was checked/parsed.
164      * <p/>
165      * This can return null in two cases:
166      * <ul>
167      * <li>The manifest was not part of the resource change delta, and the manifest was
168      * not checked/parsed ({@link #getCheckedManifestXml()} returns <code>false</code>)</li>
169      * <li>The manifest was parsed ({@link #getCheckedManifestXml()} returns <code>true</code>),
170      * but the package declaration is missing</li>
171      * </ul>
172      * @return the minSdkVersion or null.
173      */
getMinSdkVersion()174     public String getMinSdkVersion() {
175         return mMinSdkVersion;
176     }
177 
178     /*
179      * (non-Javadoc)
180      *
181      * @see org.eclipse.core.resources.IResourceDeltaVisitor
182      *      #visit(org.eclipse.core.resources.IResourceDelta)
183      */
visit(IResourceDelta delta)184     public boolean visit(IResourceDelta delta) throws CoreException {
185         // we are only going to look for changes in res/, source folders and in
186         // AndroidManifest.xml since the delta visitor goes through the main
187         // folder before its children we can check when the path segment
188         // count is 2 (format will be /$Project/folder) and make sure we are
189         // processing res/, source folders or AndroidManifest.xml
190 
191         IResource resource = delta.getResource();
192         IPath path = resource.getFullPath();
193         String[] segments = path.segments();
194 
195         // since the delta visitor also visits the root we return true if
196         // segments.length = 1
197         if (segments.length == 1) {
198             // FIXME: check this is an Android project.
199             return true;
200         } else if (segments.length == 2) {
201             // if we are at an item directly under the root directory,
202             // then we are not yet in a source or resource folder
203             mInRes = false;
204             mSourceFolder = null;
205 
206             if (SdkConstants.FD_RESOURCES.equalsIgnoreCase(segments[1])) {
207                 // this is the resource folder that was modified. we want to
208                 // see its content.
209 
210                 // since we're going to visit its children next, we set the
211                 // flag
212                 mInRes = true;
213                 mSourceFolder = null;
214                 return true;
215             } else if (AndroidConstants.FN_ANDROID_MANIFEST.equalsIgnoreCase(segments[1])) {
216                 // any change in the manifest could trigger a new R.java
217                 // class, so we don't need to check the delta kind
218                 if (delta.getKind() != IResourceDelta.REMOVED) {
219                     // parse the manifest for errors
220                     AndroidManifestParser parser = BaseProjectHelper.parseManifestForError(
221                             (IFile)resource, this);
222 
223                     if (parser != null) {
224                         mJavaPackage = parser.getPackage();
225                         mMinSdkVersion = parser.getApiLevelRequirement();
226                     }
227 
228                     mCheckedManifestXml = true;
229                 }
230                 mCompileResources = true;
231 
232                 // we don't want to go to the children, not like they are
233                 // any for this resource anyway.
234                 return false;
235             }
236         }
237 
238         // at this point we can either be in the source folder or in the
239         // resource folder or in a different folder that contains a source
240         // folder.
241         // This is due to not all source folder being src/. Some could be
242         // something/somethingelse/src/
243 
244         // so first we test if we already know we are in a source or
245         // resource folder.
246 
247         if (mSourceFolder != null) {
248             // if we are in the res folder, we are looking for the following changes:
249             // - added/removed/modified aidl files.
250             // - missing R.java file
251 
252             // if the resource is a folder, we just go straight to the children
253             if (resource.getType() == IResource.FOLDER) {
254                 return true;
255             }
256 
257             if (resource.getType() != IResource.FILE) {
258                 return false;
259             }
260             IFile file = (IFile)resource;
261 
262             // get the modification kind
263             int kind = delta.getKind();
264 
265             // we process normal source folder and the 'gen' source folder differently.
266             if (mIsGenSourceFolder) {
267                 // this is the generated java file source folder.
268                 // - if R.java/Manifest.java are removed/modified, we recompile the resources
269                 // - if aidl files are removed/modified, we recompile them.
270 
271                 boolean outputWarning = false;
272 
273                 String fileName = resource.getName();
274 
275                 // Special case of R.java/Manifest.java.
276                 if (AndroidConstants.FN_RESOURCE_CLASS.equals(fileName) ||
277                         AndroidConstants.FN_MANIFEST_CLASS.equals(fileName)) {
278                     // if it was removed, there's a possibility that it was removed due to a
279                     // package change, or an aidl that was removed, but the only thing
280                     // that will happen is that we'll have an extra build. Not much of a problem.
281                     mCompileResources = true;
282 
283                     // we want a warning
284                     outputWarning = true;
285                 } else {
286                     // this has to be a Java file created from an aidl file.
287                     // Look for the source aidl file in all the source folders.
288                     String aidlFileName = fileName.replaceAll(AndroidConstants.RE_JAVA_EXT,
289                             AndroidConstants.DOT_AIDL);
290 
291                     for (IPath sourceFolderPath : mSourceFolders) {
292                         // do not search in the current source folder as it is the 'gen' folder.
293                         if (sourceFolderPath.equals(mSourceFolder.getFullPath())) {
294                             continue;
295                         }
296 
297                         IFolder sourceFolder = getFolder(sourceFolderPath);
298                         if (sourceFolder != null) {
299                             // go recursively, segment by segment.
300                             // index starts at 2 (0 is project, 1 is 'gen'
301                             IFile sourceFile = findFile(sourceFolder, segments, 2, aidlFileName);
302 
303                             if (sourceFile != null) {
304                                 // found the source. add it to the list of files to compile
305                                 mAidlToCompile.add(new AidlData(sourceFolder, sourceFile));
306                                 outputWarning = true;
307                                 break;
308                             }
309                         }
310                     }
311                 }
312 
313                 if (outputWarning) {
314                     if (kind == IResourceDelta.REMOVED) {
315                         // We pring an error just so that it's red, but it's just a warning really.
316                         String msg = String.format(Messages.s_Removed_Recreating_s, fileName);
317                         AdtPlugin.printErrorToConsole(mBuilder.getProject(), msg);
318                     } else if (kind == IResourceDelta.CHANGED) {
319                         // the file was modified manually! we can't allow it.
320                         String msg = String.format(Messages.s_Modified_Manually_Recreating_s,
321                                 fileName);
322                         AdtPlugin.printErrorToConsole(mBuilder.getProject(), msg);
323                     }
324                 }
325             } else {
326                 // this is another source folder.
327                 // We only care about aidl files being added/modified/removed.
328 
329                 // get the extension of the resource
330                 String ext = resource.getFileExtension();
331                 if (AndroidConstants.EXT_AIDL.equalsIgnoreCase(ext)) {
332                     // first check whether it's a regular file or a parcelable.
333                     AidlType type = getAidlType(file);
334 
335                     if (type == AidlType.INTERFACE) {
336                         if (kind == IResourceDelta.REMOVED) {
337                             // we'll have to remove the generated file.
338                             mAidlToRemove.add(new AidlData(mSourceFolder, file));
339                         } else if (mForceAidlCompile == false) {
340                             // add the aidl file to the list of file to (re)compile
341                             mAidlToCompile.add(new AidlData(mSourceFolder, file));
342                         }
343                     } else {
344                         // force recompilations of all Aidl Files.
345                         mForceAidlCompile = true;
346                         mAidlToCompile.clear();
347                     }
348                 }
349             }
350 
351             // no children.
352             return false;
353         } else if (mInRes) {
354             // if we are in the res folder, we are looking for the following
355             // changes:
356             // - added/removed/modified xml files.
357             // - added/removed files of any other type
358 
359             // if the resource is a folder, we just go straight to the
360             // children
361             if (resource.getType() == IResource.FOLDER) {
362                 return true;
363             }
364 
365             // get the extension of the resource
366             String ext = resource.getFileExtension();
367             int kind = delta.getKind();
368 
369             String p = resource.getProjectRelativePath().toString();
370             String message = null;
371             switch (kind) {
372                 case IResourceDelta.CHANGED:
373                     // display verbose message
374                     message = String.format(Messages.s_Modified_Recreating_s, p,
375                             AndroidConstants.FN_RESOURCE_CLASS);
376                     break;
377                 case IResourceDelta.ADDED:
378                     // display verbose message
379                     message = String.format(Messages.Added_s_s_Needs_Updating, p,
380                             AndroidConstants.FN_RESOURCE_CLASS);
381                     break;
382                 case IResourceDelta.REMOVED:
383                     // display verbose message
384                     message = String.format(Messages.s_Removed_s_Needs_Updating, p,
385                             AndroidConstants.FN_RESOURCE_CLASS);
386                     break;
387             }
388             if (message != null) {
389                 AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE,
390                         mBuilder.getProject(), message);
391             }
392 
393             if (AndroidConstants.EXT_XML.equalsIgnoreCase(ext)) {
394                 if (kind != IResourceDelta.REMOVED) {
395                     // check xml Validity
396                     mBuilder.checkXML(resource, this);
397                 }
398 
399                 // if we are going through this resource, it was modified
400                 // somehow.
401                 // we don't care if it was an added/removed/changed event
402                 mCompileResources = true;
403                 return false;
404             } else {
405                 // this is a non xml resource.
406                 if (kind == IResourceDelta.ADDED
407                         || kind == IResourceDelta.REMOVED) {
408                     mCompileResources = true;
409                     return false;
410                 }
411             }
412         } else if (resource instanceof IFolder) {
413             // in this case we may be inside a folder that contains a source
414             // folder, go through the list of known source folders
415 
416             for (IPath sourceFolderPath : mSourceFolders) {
417                 // first check if they match exactly.
418                 if (sourceFolderPath.equals(path)) {
419                     // this is a source folder!
420                     mInRes = false;
421                     mSourceFolder = getFolder(sourceFolderPath); // all non null due to test above
422                     mIsGenSourceFolder = path.segmentCount() == 2 &&
423                             path.segment(1).equals(SdkConstants.FD_GEN_SOURCES);
424                     return true;
425                 }
426 
427                 // check if we are on the way to a source folder.
428                 int count = sourceFolderPath.matchingFirstSegments(path);
429                 if (count == path.segmentCount()) {
430                     mInRes = false;
431                     return true;
432                 }
433             }
434 
435             // if we're here, we are visiting another folder
436             // like /$Project/bin/ for instance (we get notified for changes
437             // in .class!)
438             // This could also be another source folder and we have found
439             // R.java in a previous source folder
440             // We don't want to visit its children
441             return false;
442         }
443 
444         return false;
445     }
446 
447     /**
448      * Searches for and return a file in a folder. The file is defined by its segments, and a new
449      * name (replacing the last segment).
450      * @param folder the folder we are searching
451      * @param segments the segments of the file to search.
452      * @param index the index of the current segment we are looking for
453      * @param filename the new name to replace the last segment.
454      * @return the {@link IFile} representing the searched file, or null if not found
455      */
findFile(IFolder folder, String[] segments, int index, String filename)456     private IFile findFile(IFolder folder, String[] segments, int index, String filename) {
457         boolean lastSegment = index == segments.length - 1;
458         IResource resource = folder.findMember(lastSegment ? filename : segments[index]);
459         if (resource != null && resource.exists()) {
460             if (lastSegment) {
461                 if (resource.getType() == IResource.FILE) {
462                     return (IFile)resource;
463                 }
464             } else {
465                 if (resource.getType() == IResource.FOLDER) {
466                     return findFile((IFolder)resource, segments, index+1, filename);
467                 }
468             }
469         }
470         return null;
471     }
472 
473     /**
474      * Returns a handle to the folder identified by the given path in this container.
475      * <p/>The different with {@link IContainer#getFolder(IPath)} is that this returns a non
476      * null object only if the resource actually exists and is a folder (and not a file)
477      * @param path the path of the folder to return.
478      * @return a handle to the folder if it exists, or null otherwise.
479      */
getFolder(IPath path)480     private IFolder getFolder(IPath path) {
481         IResource resource = mRoot.findMember(path);
482         if (resource != null && resource.exists() && resource.getType() == IResource.FOLDER) {
483             return (IFolder)resource;
484         }
485 
486         return null;
487     }
488 
489     /**
490      * Returns the type of the aidl file. Aidl files can either declare interfaces, or declare
491      * parcelables. This method will attempt to parse the file and return the type. If the type
492      * cannot be determined, then it will return {@link AidlType#UNKNOWN}.
493      * @param file The aidl file
494      * @return the type of the aidl.
495      * @throws CoreException
496      */
getAidlType(IFile file)497     private AidlType getAidlType(IFile file) throws CoreException {
498         // At this time, parsing isn't available, so we return UNKNOWN. This will force
499         // a recompilation of all aidl file as soon as one is changed.
500         return AidlType.UNKNOWN;
501 
502         // TODO: properly parse aidl file to determine type and generate dependency graphs.
503 //
504 //        String className = file.getName().substring(0,
505 //                file.getName().length() - AndroidConstants.DOT_AIDL.length());
506 //
507 //        InputStream input = file.getContents(true /* force*/);
508 //        try {
509 //            BufferedReader reader = new BufferedReader(new InputStreamReader(input));
510 //            String line;
511 //            while ((line = reader.readLine()) != null) {
512 //                if (line.length() == 0) {
513 //                    continue;
514 //                }
515 //
516 //                Matcher m = sParcelablePattern.matcher(line);
517 //                if (m.matches() && m.group(1).equals(className)) {
518 //                    return AidlType.PARCELABLE;
519 //                }
520 //
521 //                m = sInterfacePattern.matcher(line);
522 //                if (m.matches() && m.group(1).equals(className)) {
523 //                    return AidlType.INTERFACE;
524 //                }
525 //            }
526 //        } catch (IOException e) {
527 //            throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
528 //                    "Error parsing aidl file", e));
529 //        } finally {
530 //            try {
531 //                input.close();
532 //            } catch (IOException e) {
533 //                throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
534 //                        "Error parsing aidl file", e));
535 //            }
536 //        }
537 //
538 //        return AidlType.UNKNOWN;
539     }
540 }
541