blob: d5e63650d013368e1c27e67040b9fe82056de7b6 [file] [log] [blame]
/*
* 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;
}
}