| /* |
| * Copyright (C) 2007 The Android Open Source Project |
| * |
| * Licensed under the Eclipse Public License, Version 1.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.eclipse.org/org/documents/epl-v10.php |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.ide.eclipse.adt.internal.build; |
| |
| import com.android.ide.eclipse.adt.AdtConstants; |
| import com.android.ide.eclipse.adt.AdtPlugin; |
| import com.android.ide.eclipse.adt.AndroidConstants; |
| import com.android.ide.eclipse.adt.internal.build.BaseBuilder.BaseDeltaVisitor; |
| import com.android.ide.eclipse.adt.internal.build.PreCompilerBuilder.AidlData; |
| import com.android.ide.eclipse.adt.internal.project.AndroidManifestParser; |
| import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; |
| import com.android.sdklib.SdkConstants; |
| |
| import org.eclipse.core.resources.IContainer; |
| import org.eclipse.core.resources.IFile; |
| import org.eclipse.core.resources.IFolder; |
| import org.eclipse.core.resources.IResource; |
| import org.eclipse.core.resources.IResourceDelta; |
| import org.eclipse.core.resources.IResourceDeltaVisitor; |
| import org.eclipse.core.resources.IWorkspaceRoot; |
| import org.eclipse.core.resources.ResourcesPlugin; |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IPath; |
| |
| import java.util.ArrayList; |
| |
| /** |
| * Resource Delta visitor for the pre-compiler. |
| * <p/>This delta visitor only cares about files that are the source or the result of actions of the |
| * {@link PreCompilerBuilder}: |
| * <ul><li>R.java/Manifest.java generated by compiling the resources</li> |
| * <li>Any Java files generated by <code>aidl</code></li></ul>. |
| * |
| * Therefore it looks for the following: |
| * <ul><li>Any modification in the resource folder</li> |
| * <li>Removed files from the source folder receiving generated Java files</li> |
| * <li>Any modification to aidl files.</li> |
| * |
| */ |
| class PreCompilerDeltaVisitor extends BaseDeltaVisitor implements |
| IResourceDeltaVisitor { |
| |
| private enum AidlType { |
| UNKNOWN, INTERFACE, PARCELABLE; |
| } |
| |
| // See comment in #getAidlType() |
| // private final static Pattern sParcelablePattern = Pattern.compile( |
| // "^\\s*parcelable\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*;\\s*$"); |
| // |
| // private final static Pattern sInterfacePattern = Pattern.compile( |
| // "^\\s*interface\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(?:\\{.*)?$"); |
| |
| // Result fields. |
| /** |
| * Compile flag. This is set to true if one of the changed/added/removed |
| * file is a resource file. Upon visiting all the delta resources, if |
| * this flag is true, then we know we'll have to compile the resources |
| * into R.java |
| */ |
| private boolean mCompileResources = false; |
| |
| /** |
| * Aidl force recompilation flag. If true, we'll attempt to recompile all aidl files. |
| */ |
| private boolean mForceAidlCompile = false; |
| |
| /** List of .aidl files found that are modified or new. */ |
| private final ArrayList<AidlData> mAidlToCompile = new ArrayList<AidlData>(); |
| |
| /** List of .aidl files that have been removed. */ |
| private final ArrayList<AidlData> mAidlToRemove = new ArrayList<AidlData>(); |
| |
| /** Manifest check/parsing flag. */ |
| private boolean mCheckedManifestXml = false; |
| |
| /** Application Package, gathered from the parsing of the manifest */ |
| private String mJavaPackage = null; |
| /** minSDKVersion attribute value, gathered from the parsing of the manifest */ |
| private String mMinSdkVersion = null; |
| |
| // Internal usage fields. |
| /** |
| * In Resource folder flag. This allows us to know if we're in the |
| * resource folder. |
| */ |
| private boolean mInRes = false; |
| |
| /** |
| * Current Source folder. This allows us to know if we're in a source |
| * folder, and which folder. |
| */ |
| private IFolder mSourceFolder = null; |
| |
| /** List of source folders. */ |
| private ArrayList<IPath> mSourceFolders; |
| private boolean mIsGenSourceFolder = false; |
| |
| private IWorkspaceRoot mRoot; |
| |
| |
| public PreCompilerDeltaVisitor(BaseBuilder builder, ArrayList<IPath> sourceFolders) { |
| super(builder); |
| mSourceFolders = sourceFolders; |
| mRoot = ResourcesPlugin.getWorkspace().getRoot(); |
| } |
| |
| public boolean getCompileResources() { |
| return mCompileResources; |
| } |
| |
| public boolean getForceAidlCompile() { |
| return mForceAidlCompile; |
| } |
| |
| public ArrayList<AidlData> getAidlToCompile() { |
| return mAidlToCompile; |
| } |
| |
| public ArrayList<AidlData> getAidlToRemove() { |
| return mAidlToRemove; |
| } |
| |
| /** |
| * Returns whether the manifest file was parsed/checked for error during the resource delta |
| * visiting. |
| */ |
| public boolean getCheckedManifestXml() { |
| return mCheckedManifestXml; |
| } |
| |
| /** |
| * Returns the manifest package if the manifest was checked/parsed. |
| * <p/> |
| * This can return null in two cases: |
| * <ul> |
| * <li>The manifest was not part of the resource change delta, and the manifest was |
| * not checked/parsed ({@link #getCheckedManifestXml()} returns <code>false</code>)</li> |
| * <li>The manifest was parsed ({@link #getCheckedManifestXml()} returns <code>true</code>), |
| * but the package declaration is missing</li> |
| * </ul> |
| * @return the manifest package or null. |
| */ |
| public String getManifestPackage() { |
| return mJavaPackage; |
| } |
| |
| /** |
| * Returns the minSDkVersion attribute from the manifest if it was checked/parsed. |
| * <p/> |
| * This can return null in two cases: |
| * <ul> |
| * <li>The manifest was not part of the resource change delta, and the manifest was |
| * not checked/parsed ({@link #getCheckedManifestXml()} returns <code>false</code>)</li> |
| * <li>The manifest was parsed ({@link #getCheckedManifestXml()} returns <code>true</code>), |
| * but the package declaration is missing</li> |
| * </ul> |
| * @return the minSdkVersion or null. |
| */ |
| public String getMinSdkVersion() { |
| return mMinSdkVersion; |
| } |
| |
| /* |
| * (non-Javadoc) |
| * |
| * @see org.eclipse.core.resources.IResourceDeltaVisitor |
| * #visit(org.eclipse.core.resources.IResourceDelta) |
| */ |
| public boolean visit(IResourceDelta delta) throws CoreException { |
| // we are only going to look for changes in res/, source folders and in |
| // AndroidManifest.xml since the delta visitor goes through the main |
| // folder before its children we can check when the path segment |
| // count is 2 (format will be /$Project/folder) and make sure we are |
| // processing res/, source folders or AndroidManifest.xml |
| |
| IResource resource = delta.getResource(); |
| IPath path = resource.getFullPath(); |
| String[] segments = path.segments(); |
| |
| // since the delta visitor also visits the root we return true if |
| // segments.length = 1 |
| if (segments.length == 1) { |
| // FIXME: check this is an Android project. |
| return true; |
| } else if (segments.length == 2) { |
| // if we are at an item directly under the root directory, |
| // then we are not yet in a source or resource folder |
| mInRes = false; |
| mSourceFolder = null; |
| |
| if (SdkConstants.FD_RESOURCES.equalsIgnoreCase(segments[1])) { |
| // this is the resource folder that was modified. we want to |
| // see its content. |
| |
| // since we're going to visit its children next, we set the |
| // flag |
| mInRes = true; |
| mSourceFolder = null; |
| return true; |
| } else if (AndroidConstants.FN_ANDROID_MANIFEST.equalsIgnoreCase(segments[1])) { |
| // any change in the manifest could trigger a new R.java |
| // class, so we don't need to check the delta kind |
| if (delta.getKind() != IResourceDelta.REMOVED) { |
| // parse the manifest for errors |
| AndroidManifestParser parser = BaseProjectHelper.parseManifestForError( |
| (IFile)resource, this); |
| |
| if (parser != null) { |
| mJavaPackage = parser.getPackage(); |
| mMinSdkVersion = parser.getApiLevelRequirement(); |
| } |
| |
| mCheckedManifestXml = true; |
| } |
| mCompileResources = true; |
| |
| // we don't want to go to the children, not like they are |
| // any for this resource anyway. |
| return false; |
| } |
| } |
| |
| // at this point we can either be in the source folder or in the |
| // resource folder or in a different folder that contains a source |
| // folder. |
| // This is due to not all source folder being src/. Some could be |
| // something/somethingelse/src/ |
| |
| // so first we test if we already know we are in a source or |
| // resource folder. |
| |
| if (mSourceFolder != null) { |
| // if we are in the res folder, we are looking for the following changes: |
| // - added/removed/modified aidl files. |
| // - missing R.java file |
| |
| // if the resource is a folder, we just go straight to the children |
| if (resource.getType() == IResource.FOLDER) { |
| return true; |
| } |
| |
| if (resource.getType() != IResource.FILE) { |
| return false; |
| } |
| IFile file = (IFile)resource; |
| |
| // get the modification kind |
| int kind = delta.getKind(); |
| |
| // we process normal source folder and the 'gen' source folder differently. |
| if (mIsGenSourceFolder) { |
| // this is the generated java file source folder. |
| // - if R.java/Manifest.java are removed/modified, we recompile the resources |
| // - if aidl files are removed/modified, we recompile them. |
| |
| boolean outputWarning = false; |
| |
| String fileName = resource.getName(); |
| |
| // Special case of R.java/Manifest.java. |
| if (AndroidConstants.FN_RESOURCE_CLASS.equals(fileName) || |
| AndroidConstants.FN_MANIFEST_CLASS.equals(fileName)) { |
| // if it was removed, there's a possibility that it was removed due to a |
| // package change, or an aidl that was removed, but the only thing |
| // that will happen is that we'll have an extra build. Not much of a problem. |
| mCompileResources = true; |
| |
| // we want a warning |
| outputWarning = true; |
| } else { |
| // this has to be a Java file created from an aidl file. |
| // Look for the source aidl file in all the source folders. |
| String aidlFileName = fileName.replaceAll(AndroidConstants.RE_JAVA_EXT, |
| AndroidConstants.DOT_AIDL); |
| |
| for (IPath sourceFolderPath : mSourceFolders) { |
| // do not search in the current source folder as it is the 'gen' folder. |
| if (sourceFolderPath.equals(mSourceFolder.getFullPath())) { |
| continue; |
| } |
| |
| IFolder sourceFolder = getFolder(sourceFolderPath); |
| if (sourceFolder != null) { |
| // go recursively, segment by segment. |
| // index starts at 2 (0 is project, 1 is 'gen' |
| IFile sourceFile = findFile(sourceFolder, segments, 2, aidlFileName); |
| |
| if (sourceFile != null) { |
| // found the source. add it to the list of files to compile |
| mAidlToCompile.add(new AidlData(sourceFolder, sourceFile)); |
| outputWarning = true; |
| break; |
| } |
| } |
| } |
| } |
| |
| if (outputWarning) { |
| if (kind == IResourceDelta.REMOVED) { |
| // We pring an error just so that it's red, but it's just a warning really. |
| String msg = String.format(Messages.s_Removed_Recreating_s, fileName); |
| AdtPlugin.printErrorToConsole(mBuilder.getProject(), msg); |
| } else if (kind == IResourceDelta.CHANGED) { |
| // the file was modified manually! we can't allow it. |
| String msg = String.format(Messages.s_Modified_Manually_Recreating_s, |
| fileName); |
| AdtPlugin.printErrorToConsole(mBuilder.getProject(), msg); |
| } |
| } |
| } else { |
| // this is another source folder. |
| // We only care about aidl files being added/modified/removed. |
| |
| // get the extension of the resource |
| String ext = resource.getFileExtension(); |
| if (AndroidConstants.EXT_AIDL.equalsIgnoreCase(ext)) { |
| // first check whether it's a regular file or a parcelable. |
| AidlType type = getAidlType(file); |
| |
| if (type == AidlType.INTERFACE) { |
| if (kind == IResourceDelta.REMOVED) { |
| // we'll have to remove the generated file. |
| mAidlToRemove.add(new AidlData(mSourceFolder, file)); |
| } else if (mForceAidlCompile == false) { |
| // add the aidl file to the list of file to (re)compile |
| mAidlToCompile.add(new AidlData(mSourceFolder, file)); |
| } |
| } else { |
| // force recompilations of all Aidl Files. |
| mForceAidlCompile = true; |
| mAidlToCompile.clear(); |
| } |
| } |
| } |
| |
| // no children. |
| return false; |
| } else if (mInRes) { |
| // if we are in the res folder, we are looking for the following |
| // changes: |
| // - added/removed/modified xml files. |
| // - added/removed files of any other type |
| |
| // if the resource is a folder, we just go straight to the |
| // children |
| if (resource.getType() == IResource.FOLDER) { |
| return true; |
| } |
| |
| // get the extension of the resource |
| String ext = resource.getFileExtension(); |
| int kind = delta.getKind(); |
| |
| String p = resource.getProjectRelativePath().toString(); |
| String message = null; |
| switch (kind) { |
| case IResourceDelta.CHANGED: |
| // display verbose message |
| message = String.format(Messages.s_Modified_Recreating_s, p, |
| AndroidConstants.FN_RESOURCE_CLASS); |
| break; |
| case IResourceDelta.ADDED: |
| // display verbose message |
| message = String.format(Messages.Added_s_s_Needs_Updating, p, |
| AndroidConstants.FN_RESOURCE_CLASS); |
| break; |
| case IResourceDelta.REMOVED: |
| // display verbose message |
| message = String.format(Messages.s_Removed_s_Needs_Updating, p, |
| AndroidConstants.FN_RESOURCE_CLASS); |
| break; |
| } |
| if (message != null) { |
| AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, |
| mBuilder.getProject(), message); |
| } |
| |
| if (AndroidConstants.EXT_XML.equalsIgnoreCase(ext)) { |
| if (kind != IResourceDelta.REMOVED) { |
| // check xml Validity |
| mBuilder.checkXML(resource, this); |
| } |
| |
| // if we are going through this resource, it was modified |
| // somehow. |
| // we don't care if it was an added/removed/changed event |
| mCompileResources = true; |
| return false; |
| } else { |
| // this is a non xml resource. |
| if (kind == IResourceDelta.ADDED |
| || kind == IResourceDelta.REMOVED) { |
| mCompileResources = true; |
| return false; |
| } |
| } |
| } else if (resource instanceof IFolder) { |
| // in this case we may be inside a folder that contains a source |
| // folder, go through the list of known source folders |
| |
| for (IPath sourceFolderPath : mSourceFolders) { |
| // first check if they match exactly. |
| if (sourceFolderPath.equals(path)) { |
| // this is a source folder! |
| mInRes = false; |
| mSourceFolder = getFolder(sourceFolderPath); // all non null due to test above |
| mIsGenSourceFolder = path.segmentCount() == 2 && |
| path.segment(1).equals(SdkConstants.FD_GEN_SOURCES); |
| return true; |
| } |
| |
| // check if we are on the way to a source folder. |
| int count = sourceFolderPath.matchingFirstSegments(path); |
| if (count == path.segmentCount()) { |
| mInRes = false; |
| return true; |
| } |
| } |
| |
| // if we're here, we are visiting another folder |
| // like /$Project/bin/ for instance (we get notified for changes |
| // in .class!) |
| // This could also be another source folder and we have found |
| // R.java in a previous source folder |
| // We don't want to visit its children |
| return false; |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Searches for and return a file in a folder. The file is defined by its segments, and a new |
| * name (replacing the last segment). |
| * @param folder the folder we are searching |
| * @param segments the segments of the file to search. |
| * @param index the index of the current segment we are looking for |
| * @param filename the new name to replace the last segment. |
| * @return the {@link IFile} representing the searched file, or null if not found |
| */ |
| private IFile findFile(IFolder folder, String[] segments, int index, String filename) { |
| boolean lastSegment = index == segments.length - 1; |
| IResource resource = folder.findMember(lastSegment ? filename : segments[index]); |
| if (resource != null && resource.exists()) { |
| if (lastSegment) { |
| if (resource.getType() == IResource.FILE) { |
| return (IFile)resource; |
| } |
| } else { |
| if (resource.getType() == IResource.FOLDER) { |
| return findFile((IFolder)resource, segments, index+1, filename); |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Returns a handle to the folder identified by the given path in this container. |
| * <p/>The different with {@link IContainer#getFolder(IPath)} is that this returns a non |
| * null object only if the resource actually exists and is a folder (and not a file) |
| * @param path the path of the folder to return. |
| * @return a handle to the folder if it exists, or null otherwise. |
| */ |
| private IFolder getFolder(IPath path) { |
| IResource resource = mRoot.findMember(path); |
| if (resource != null && resource.exists() && resource.getType() == IResource.FOLDER) { |
| return (IFolder)resource; |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Returns the type of the aidl file. Aidl files can either declare interfaces, or declare |
| * parcelables. This method will attempt to parse the file and return the type. If the type |
| * cannot be determined, then it will return {@link AidlType#UNKNOWN}. |
| * @param file The aidl file |
| * @return the type of the aidl. |
| * @throws CoreException |
| */ |
| private AidlType getAidlType(IFile file) throws CoreException { |
| // At this time, parsing isn't available, so we return UNKNOWN. This will force |
| // a recompilation of all aidl file as soon as one is changed. |
| return AidlType.UNKNOWN; |
| |
| // TODO: properly parse aidl file to determine type and generate dependency graphs. |
| // |
| // String className = file.getName().substring(0, |
| // file.getName().length() - AndroidConstants.DOT_AIDL.length()); |
| // |
| // InputStream input = file.getContents(true /* force*/); |
| // try { |
| // BufferedReader reader = new BufferedReader(new InputStreamReader(input)); |
| // String line; |
| // while ((line = reader.readLine()) != null) { |
| // if (line.length() == 0) { |
| // continue; |
| // } |
| // |
| // Matcher m = sParcelablePattern.matcher(line); |
| // if (m.matches() && m.group(1).equals(className)) { |
| // return AidlType.PARCELABLE; |
| // } |
| // |
| // m = sInterfacePattern.matcher(line); |
| // if (m.matches() && m.group(1).equals(className)) { |
| // return AidlType.INTERFACE; |
| // } |
| // } |
| // } catch (IOException e) { |
| // throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, |
| // "Error parsing aidl file", e)); |
| // } finally { |
| // try { |
| // input.close(); |
| // } catch (IOException e) { |
| // throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, |
| // "Error parsing aidl file", e)); |
| // } |
| // } |
| // |
| // return AidlType.UNKNOWN; |
| } |
| } |