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
17package com.android.ide.eclipse.adt.internal.build.builders;
18
19import com.android.SdkConstants;
20import com.android.ide.common.xml.ManifestData;
21import com.android.ide.eclipse.adt.AdtConstants;
22import com.android.ide.eclipse.adt.AdtPlugin;
23import com.android.ide.eclipse.adt.internal.build.Messages;
24import com.android.ide.eclipse.adt.internal.build.SourceChangeHandler;
25import com.android.ide.eclipse.adt.internal.build.builders.BaseBuilder.BaseDeltaVisitor;
26import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity;
27import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper;
28import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
29import com.android.ide.eclipse.adt.io.IFileWrapper;
30import com.google.common.collect.Lists;
31
32import org.eclipse.core.resources.IContainer;
33import org.eclipse.core.resources.IFile;
34import org.eclipse.core.resources.IFolder;
35import org.eclipse.core.resources.IResource;
36import org.eclipse.core.resources.IResourceDelta;
37import org.eclipse.core.resources.IResourceDeltaVisitor;
38import org.eclipse.core.resources.IWorkspaceRoot;
39import org.eclipse.core.resources.ResourcesPlugin;
40import org.eclipse.core.runtime.CoreException;
41import org.eclipse.core.runtime.IPath;
42
43import java.util.Arrays;
44import java.util.List;
45
46/**
47 * Resource Delta visitor for the pre-compiler.
48 * <p/>This delta visitor only cares about files that are the source or the result of actions of the
49 * {@link PreCompilerBuilder}:
50 * <ul><li>R.java/Manifest.java generated by compiling the resources</li>
51 * <li>Any Java files generated by <code>aidl</code></li></ul>.
52 *
53 * Therefore it looks for the following:
54 * <ul><li>Any modification in the resource folder</li>
55 * <li>Removed files from the source folder receiving generated Java files</li>
56 * <li>Any modification to aidl files.</li>
57 *
58 */
59class PreCompilerDeltaVisitor extends BaseDeltaVisitor implements IResourceDeltaVisitor {
60
61    // Result fields.
62    private boolean mChangedManifest = false;
63
64    /**
65     * Compile flag. This is set to true if one of the changed/added/removed
66     * files is Manifest.java, or R.java. All other file changes
67     * will be taken care of by ResourceManager.
68     */
69    private boolean mCompileResources = false;
70
71    /** Manifest check/parsing flag. */
72    private boolean mCheckedManifestXml = false;
73
74    /** Application Package, gathered from the parsing of the manifest */
75    private String mJavaPackage = null;
76    /** minSDKVersion attribute value, gathered from the parsing of the manifest */
77    private String mMinSdkVersion = null;
78
79    // Internal usage fields.
80    /**
81     * In Resource folder flag. This allows us to know if we're in the
82     * resource folder.
83     */
84    private boolean mInRes = false;
85
86    /**
87     * Current Source folder. This allows us to know if we're in a source
88     * folder, and which folder.
89     */
90    private IFolder mSourceFolder = null;
91
92    /** List of source folders. */
93    private final List<IPath> mSourceFolders;
94    private boolean mIsGenSourceFolder = false;
95
96    private final List<SourceChangeHandler> mSourceChangeHandlers = Lists.newArrayList();
97    private final IWorkspaceRoot mRoot;
98
99    private IFolder mAndroidOutputFolder;
100
101    public PreCompilerDeltaVisitor(BaseBuilder builder, List<IPath> sourceFolders,
102            SourceChangeHandler... handlers) {
103        super(builder);
104        mSourceFolders = sourceFolders;
105        mRoot = ResourcesPlugin.getWorkspace().getRoot();
106
107        mSourceChangeHandlers.addAll(Arrays.asList(handlers));
108
109        mAndroidOutputFolder = BaseProjectHelper.getAndroidOutputFolder(builder.getProject());
110    }
111
112    /**
113     * Get whether Manifest.java, Manifest.xml, or R.java have changed
114     * @return true if any of Manifest.xml, Manifest.java, or R.java have been modified
115     */
116    public boolean getCompileResources() {
117        return mCompileResources || mChangedManifest;
118    }
119
120    public boolean hasManifestChanged() {
121        return mChangedManifest;
122    }
123
124    /**
125     * Returns whether the manifest file was parsed/checked for error during the resource delta
126     * visiting.
127     */
128    public boolean getCheckedManifestXml() {
129        return mCheckedManifestXml;
130    }
131
132    /**
133     * Returns the manifest package if the manifest was checked/parsed.
134     * <p/>
135     * This can return null in two cases:
136     * <ul>
137     * <li>The manifest was not part of the resource change delta, and the manifest was
138     * not checked/parsed ({@link #getCheckedManifestXml()} returns <code>false</code>)</li>
139     * <li>The manifest was parsed ({@link #getCheckedManifestXml()} returns <code>true</code>),
140     * but the package declaration is missing</li>
141     * </ul>
142     * @return the manifest package or null.
143     */
144    public String getManifestPackage() {
145        return mJavaPackage;
146    }
147
148    /**
149     * Returns the minSDkVersion attribute from the manifest if it was checked/parsed.
150     * <p/>
151     * This can return null in two cases:
152     * <ul>
153     * <li>The manifest was not part of the resource change delta, and the manifest was
154     * not checked/parsed ({@link #getCheckedManifestXml()} returns <code>false</code>)</li>
155     * <li>The manifest was parsed ({@link #getCheckedManifestXml()} returns <code>true</code>),
156     * but the package declaration is missing</li>
157     * </ul>
158     * @return the minSdkVersion or null.
159     */
160    public String getMinSdkVersion() {
161        return mMinSdkVersion;
162    }
163
164    /*
165     * (non-Javadoc)
166     *
167     * @see org.eclipse.core.resources.IResourceDeltaVisitor
168     *      #visit(org.eclipse.core.resources.IResourceDelta)
169     */
170    @Override
171    public boolean visit(IResourceDelta delta) throws CoreException {
172        // we are only going to look for changes in res/, source folders and in
173        // AndroidManifest.xml since the delta visitor goes through the main
174        // folder before its children we can check when the path segment
175        // count is 2 (format will be /$Project/folder) and make sure we are
176        // processing res/, source folders or AndroidManifest.xml
177
178        IResource resource = delta.getResource();
179        IPath path = resource.getFullPath();
180        String[] segments = path.segments();
181
182        // since the delta visitor also visits the root we return true if
183        // segments.length = 1
184        if (segments.length == 1) {
185            // this is always the Android project since we call
186            // Builder#getDelta(IProject) on the project itself.
187            return true;
188        } else if (segments.length == 2) {
189            // if we are at an item directly under the root directory,
190            // then we are not yet in a source or resource folder
191            mInRes = false;
192            mSourceFolder = null;
193
194            if (SdkConstants.FD_RESOURCES.equalsIgnoreCase(segments[1])) {
195                // this is the resource folder that was modified. we want to
196                // see its content.
197
198                // since we're going to visit its children next, we set the
199                // flag
200                mInRes = true;
201                mSourceFolder = null;
202                return true;
203            } else if (SdkConstants.FN_ANDROID_MANIFEST_XML.equalsIgnoreCase(segments[1])) {
204                // any change in the manifest could trigger a new R.java
205                // class, so we don't need to check the delta kind
206                if (delta.getKind() != IResourceDelta.REMOVED) {
207                    // clean the error markers on the file.
208                    IFile manifestFile = (IFile)resource;
209
210                    if (manifestFile.exists()) {
211                        manifestFile.deleteMarkers(AdtConstants.MARKER_XML, true,
212                                IResource.DEPTH_ZERO);
213                        manifestFile.deleteMarkers(AdtConstants.MARKER_ANDROID, true,
214                                IResource.DEPTH_ZERO);
215                    }
216
217                    // parse the manifest for data and error
218                    ManifestData manifestData = AndroidManifestHelper.parse(
219                            new IFileWrapper(manifestFile), true /*gatherData*/, this);
220
221                    if (manifestData != null) {
222                        mJavaPackage = manifestData.getPackage();
223                        mMinSdkVersion = manifestData.getMinSdkVersionString();
224                    }
225
226                    mCheckedManifestXml = true;
227                }
228                mChangedManifest = true;
229
230                // we don't want to go to the children, not like they are
231                // any for this resource anyway.
232                return false;
233            }
234        }
235
236        // at this point we can either be in the source folder or in the
237        // resource folder or in a different folder that contains a source
238        // folder.
239        // This is due to not all source folder being src/. Some could be
240        // something/somethingelse/src/
241
242        // so first we test if we already know we are in a source or
243        // resource folder.
244
245        if (mSourceFolder != null) {
246            // if we are in the res folder, we are looking for the following changes:
247            // - added/removed/modified aidl files.
248            // - missing R.java file
249
250            // if the resource is a folder, we just go straight to the children
251            if (resource.getType() == IResource.FOLDER) {
252                return true;
253            }
254
255            if (resource.getType() != IResource.FILE) {
256                return false;
257            }
258            IFile file = (IFile)resource;
259
260            // get the modification kind
261            int kind = delta.getKind();
262
263            // we process normal source folder and the 'gen' source folder differently.
264            if (mIsGenSourceFolder) {
265                // this is the generated java file source folder.
266                // - if R.java/Manifest.java are removed/modified, we recompile the resources
267                // - if aidl files are removed/modified, we recompile them.
268
269                boolean outputWarning = false;
270
271                String fileName = resource.getName();
272
273                // Special case of R.java/Manifest.java.
274                if (SdkConstants.FN_RESOURCE_CLASS.equals(fileName) ||
275                        SdkConstants.FN_MANIFEST_CLASS.equals(fileName)) {
276                    // if it was removed, there's a possibility that it was removed due to a
277                    // package change, or an aidl that was removed, but the only thing
278                    // that will happen is that we'll have an extra build. Not much of a problem.
279                    mCompileResources = true;
280
281                    // we want a warning
282                    outputWarning = true;
283                } else {
284                    // look to see if this file was generated by a processor.
285                    for (SourceChangeHandler handler : mSourceChangeHandlers) {
286                        if (handler.handleGeneratedFile(file, kind)) {
287                            outputWarning = true;
288                            break; // there shouldn't be 2 processors that handle the same file.
289                        }
290                    }
291                }
292
293                if (outputWarning) {
294                    if (kind == IResourceDelta.REMOVED) {
295                        // We print an error just so that it's red, but it's just a warning really.
296                        String msg = String.format(Messages.s_Removed_Recreating_s, fileName);
297                        AdtPlugin.printErrorToConsole(mBuilder.getProject(), msg);
298                    } else if (kind == IResourceDelta.CHANGED) {
299                        // the file was modified manually! we can't allow it.
300                        String msg = String.format(Messages.s_Modified_Manually_Recreating_s,
301                                fileName);
302                        AdtPlugin.printErrorToConsole(mBuilder.getProject(), msg);
303                    }
304                }
305            } else {
306                // this is another source folder.
307                for (SourceChangeHandler handler : mSourceChangeHandlers) {
308                    handler.handleSourceFile(file, kind);
309                }
310            }
311
312            // no children.
313            return false;
314        } else if (mInRes) {
315            // if we are in the res folder, we are looking for the following
316            // changes:
317            // - added/removed/modified xml files.
318            // - added/removed files of any other type
319
320            // if the resource is a folder, we just go straight to the
321            // children
322            if (resource.getType() == IResource.FOLDER) {
323                return true;
324            }
325
326            // get the extension of the resource
327            String ext = resource.getFileExtension();
328            int kind = delta.getKind();
329
330            String p = resource.getProjectRelativePath().toString();
331            String message = null;
332            switch (kind) {
333                case IResourceDelta.CHANGED:
334                    // display verbose message
335                    message = String.format(Messages.s_Modified_Recreating_s, p);
336                    break;
337                case IResourceDelta.ADDED:
338                    // display verbose message
339                    message = String.format(Messages.Added_s_s_Needs_Updating, p,
340                            SdkConstants.FN_RESOURCE_CLASS);
341                    break;
342                case IResourceDelta.REMOVED:
343                    // display verbose message
344                    message = String.format(Messages.s_Removed_s_Needs_Updating, p,
345                            SdkConstants.FN_RESOURCE_CLASS);
346                    break;
347            }
348            if (message != null) {
349                AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE,
350                        mBuilder.getProject(), message);
351            }
352
353            // If it's an XML resource, check the syntax
354            if (SdkConstants.EXT_XML.equalsIgnoreCase(ext) && kind != IResourceDelta.REMOVED) {
355                // check xml Validity
356                mBuilder.checkXML(resource, this);
357            }
358            // Whether or not to generate R.java for a changed resource is taken care of by the
359            // Resource Manager.
360        } else if (resource instanceof IFolder) {
361            // first check if we are in the android output folder.
362            if (resource.equals(mAndroidOutputFolder)) {
363                // we want to visit the merged manifest.
364                return true;
365            }
366
367            // in this case we may be inside a folder that contains a source
368            // folder, go through the list of known source folders
369
370            for (IPath sourceFolderPath : mSourceFolders) {
371                // first check if they match exactly.
372                if (sourceFolderPath.equals(path)) {
373                    // this is a source folder!
374                    mInRes = false;
375                    mSourceFolder = getFolder(sourceFolderPath); // all non null due to test above
376                    mIsGenSourceFolder = path.segmentCount() == 2 &&
377                            path.segment(1).equals(SdkConstants.FD_GEN_SOURCES);
378                    return true;
379                }
380
381                // check if we are on the way to a source folder.
382                int count = sourceFolderPath.matchingFirstSegments(path);
383                if (count == path.segmentCount()) {
384                    mInRes = false;
385                    return true;
386                }
387            }
388
389            // if we're here, we are visiting another folder
390            // like /$Project/bin/ for instance (we get notified for changes
391            // in .class!)
392            // This could also be another source folder and we have found
393            // R.java in a previous source folder
394            // We don't want to visit its children
395            return false;
396        }
397
398        return false;
399    }
400
401    /**
402     * Returns a handle to the folder identified by the given path in this container.
403     * <p/>The different with {@link IContainer#getFolder(IPath)} is that this returns a non
404     * null object only if the resource actually exists and is a folder (and not a file)
405     * @param path the path of the folder to return.
406     * @return a handle to the folder if it exists, or null otherwise.
407     */
408    private IFolder getFolder(IPath path) {
409        IResource resource = mRoot.findMember(path);
410        if (resource != null && resource.exists() && resource.getType() == IResource.FOLDER) {
411            return (IFolder)resource;
412        }
413
414        return null;
415    }
416
417}
418