blob: 8162fa0365d1c4739440c03fae86e6d4cfcee4da [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
*
* 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.tools.idea.gradle.eclipse;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.android.sdklib.AndroidTargetHash;
import com.android.sdklib.AndroidVersion;
import com.android.sdklib.SdkVersionInfo;
import com.android.tools.idea.gradle.util.PropertiesUtil;
import com.android.tools.lint.detector.api.LintUtils;
import com.android.utils.SdkUtils;
import com.android.utils.XmlUtils;
import com.google.common.base.Charsets;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.io.Files;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.android.SdkConstants.*;
import static com.android.sdklib.internal.project.ProjectProperties.PROPERTY_SDK;
import static com.android.tools.idea.gradle.eclipse.ImportModule.*;
import static com.android.utils.SdkUtils.endsWithIgnoreCase;
import static com.android.xml.AndroidManifest.*;
import static java.io.File.separator;
import static java.io.File.separatorChar;
/** Provides information about an Eclipse project */
class EclipseProject implements Comparable<EclipseProject> {
static final String DEFAULT_LANGUAGE_LEVEL = "1.6";
private static final String HOME_PROPERTY = "user.home"; //$NON-NLS-1$
private static final String HOME_PROPERTY_REF = "${" + HOME_PROPERTY + '}'; //$NON-NLS-1$
private static final String SDK_PROPERTY_REF = "${" + PROPERTY_SDK + '}'; //$NON-NLS-1$
private final GradleImport myImporter;
private final File myDir;
private final File myCanonicalDir;
private boolean myLibrary;
private boolean myAndroidProject;
private boolean myNdkProject;
private AndroidVersion myMinSdkVersion;
private AndroidVersion myTargetSdkVersion;
private Document myProjectDoc;
private Document myManifestDoc;
private Properties myProjectProperties;
private AndroidVersion myVersion;
private String myAddOn;
private String myName;
private String myLanguageLevel;
private List<EclipseProject> myDirectLibraries;
private List<File> mySourcePaths;
private List<File> myJarPaths;
private List<File> myInstrumentationJarPaths;
private List<File> myNativeLibs;
private List<String> myInferredLibraries;
private File myNativeSources;
private String myNativeModuleName;
private File myOutputDir;
private String myPackage;
private List<File> myLocalProguardFiles;
private List<File> mySdkProguardFiles;
private List<EclipseProject> myAllLibraries;
private EclipseImportModule myModule;
private Map<String, String> myProjectVariableMap;
private Map<String, String> myLinkedResourceMap;
private File myInstrumentationDir;
private Map<File, Charset> myFileCharsets;
private Charset myDefaultCharset;
private EclipseProject(@NonNull GradleImport importer, @NonNull File dir) throws IOException {
myImporter = importer;
myDir = dir;
myCanonicalDir = dir.getCanonicalFile();
// Ensure that the library references (which are canonicalized) find this project
// if included from multiple locations
myImporter.registerProject(this);
initProjectName();
initAndroidProject();
initLanguageLevel();
if (isAndroidProject()) {
Properties properties = getProjectProperties();
if (properties != null) {
initProguard(properties);
initVersion(properties);
initLibraries(properties);
initLibrary(properties);
}
initPackage();
initMinSdkVersion();
initInstrumentation();
}
else {
myDirectLibraries = new ArrayList<EclipseProject>(4);
}
initClassPathEntries();
initJni();
initEncoding();
}
@NonNull
public static EclipseProject getProject(@NonNull GradleImport importer, @NonNull File dir) throws IOException {
Map<File, EclipseProject> mProjectMap = importer.getProjectMap();
EclipseProject project = mProjectMap.get(dir);
if (project == null) {
project = createProject(importer, dir);
// The project should register itself in the map; we don't have to do that here.
// (The code used to do that here, but it turns out project creation can recursively
// visit library references as part of initialization, so have the projects register
// themselves prior to initialization instead)
assert mProjectMap.get(dir) != null;
}
return project;
}
@NonNull
private static EclipseProject createProject(@NonNull GradleImport importer, @NonNull File dir) throws IOException {
// Read the .classpath, .project, project.properties and local.properties files (if there)
return new EclipseProject(importer, dir);
}
@Nullable
private static AndroidVersion getApiVersion(Element usesSdk, String attribute, @Nullable AndroidVersion defaultApiLevel) {
String valueString = null;
if (usesSdk.hasAttributeNS(ANDROID_URI, attribute)) {
valueString = usesSdk.getAttributeNS(ANDROID_URI, attribute);
}
if (valueString != null) {
AndroidVersion version = SdkVersionInfo.getVersion(valueString, null);
if (version != null) {
return version;
}
}
return defaultApiLevel;
}
@Nullable
private static String getInstrumentationTarget(@NonNull GradleImport importer, @NonNull File manifest) throws IOException {
Document doc = importer.getXmlDocument(manifest, true);
if (doc != null) {
NodeList list = doc.getElementsByTagName(NODE_INSTRUMENTATION);
for (int i = 0; i < list.getLength(); i++) {
Element tag = (Element)list.item(i);
String target = tag.getAttributeNS(ANDROID_URI, ATTRIBUTE_TARGET_PACKAGE);
if (target != null && !target.isEmpty()) {
return target;
}
}
}
return null;
}
@Nullable
private static String getStringValue(@NonNull Element element) {
NodeList children = element.getChildNodes();
for (int j = 0; j < children.getLength(); j++) {
Node child = children.item(j);
if (child.getNodeType() == Node.TEXT_NODE) {
return child.getNodeValue().trim();
}
}
return null;
}
/**
* Creates a list of modules from the given set of projects. The returned list
* is in dependency order.
*/
public static List<? extends ImportModule> performImport(@NonNull GradleImport importer, @NonNull Collection<EclipseProject> projects) {
List<EclipseImportModule> modules = Lists.newArrayList();
List<EclipseImportModule> replacedByDependencies = Lists.newArrayList();
for (EclipseProject project : projects) {
EclipseImportModule module = new EclipseImportModule(importer, project);
module.initialize();
if (module.isReplacedWithDependency()) {
replacedByDependencies.add(module);
}
else {
modules.add(module);
}
}
// Some libraries may be replaced by just a dependency (for example,
// instead of copying in a whole copy of ActionBarSherlock, just
// replace by the corresponding dependency.
for (EclipseImportModule replaced : replacedByDependencies) {
assert replaced.getReplaceWithDependencies() != null;
EclipseProject project = replaced.getProject();
for (EclipseImportModule module : modules) {
if (module.getProject().getAllLibraries().contains(project)) {
module.addDependencies(replaced.getReplaceWithDependencies());
}
}
}
// Strip out .jar files from the libs/ folder if already implied by
// library dependencies
for (EclipseImportModule module : modules) {
module.removeJarDependencies();
}
// Sort by dependency order
Collections.sort(modules);
return modules;
}
private void initVersion(@NonNull Properties properties) {
String target = properties.getProperty("target"); //$NON-NLS-1$
if (target != null) {
myVersion = AndroidTargetHash.getPlatformVersion(target);
// getPlatformVersion does not handle API numbers correctly
if (myVersion != null && myVersion.isPreview()) {
// Update codename
AndroidVersion version = SdkVersionInfo.getVersion(myVersion.getCodename(), null);
if (version != null) {
myVersion = version;
}
} else {
int index = target.lastIndexOf(':');
if (index != -1) {
myVersion = SdkVersionInfo.getVersion(target.substring(index + 1), null);
myAddOn = target;
}
}
}
}
private void initLibraries(@NonNull Properties properties) throws IOException {
myDirectLibraries = new ArrayList<EclipseProject>(4);
for (int i = 0; i < 1000; i++) {
String key = String.format(ANDROID_LIBRARY_REFERENCE_FORMAT, i);
String library = properties.getProperty(key);
if (library == null || library.isEmpty()) {
// No holes in the numbering sequence is allowed
if (i == 0) {
// Except for i=0; library projects are supposed to start with 1, and
// all the ADT, sdklib and ant code which reads and writes these start with
// 1, but I've encountered several projects in the wild that start with 0;
// presumably from manual edits or because some older version of the tools
// did this.
// Instead of bailing here, try 1 too.
continue;
}
else {
// After 1, we don't allow any gaps in the sequence
break;
}
}
// Handle importing Windows-relative paths in project.properties on non-Windows,
// and vice versa
if (CURRENT_PLATFORM == PLATFORM_WINDOWS) {
library = library.replace('/', '\\');
}
else {
library = library.replace('\\', '/');
}
File path = new File(library);
File joined = path.isAbsolute() ? path : new File(myDir, library);
File libraryDir = joined.getCanonicalFile();
if (!libraryDir.exists()) {
if (myImporter.isReplaceLibs()) {
// Look for some common libraries that we can probably just guess as a dependency replacement
String libraryDirName = libraryDir.getName().replace('_', '-');
if (libraryDirName.indexOf('-') == -1) {
File parent = libraryDir.getParentFile();
if (parent != null) {
String parentName = parent.getName();
if (parentName.equals("v7")) {
// Recognize paths pointing into an SDK extras folder such as
// ../../../android-sdk-mac_86/extras/android/compatibility/v7/appcompat
// and turn them into appcompat-v7 such that they match the artifact check below
libraryDirName = libraryDirName + '-' + parentName;
}
}
}
if (APPCOMPAT_ARTIFACT.equals(libraryDirName)
|| SUPPORT_ARTIFACT.equals(libraryDirName)
|| GRIDLAYOUT_ARTIFACT.equals(libraryDirName)
|| MEDIA_ROUTER_ARTIFACT.equals(libraryDirName)) {
if (myInferredLibraries == null) {
myInferredLibraries = Lists.newArrayList();
}
myInferredLibraries.add(libraryDirName);
continue;
}
}
String message = "Library reference " + library + " could not be found";
if (!path.isAbsolute()) {
message += "\nPath is " + joined + " which resolves to " + libraryDir.getPath();
}
myImporter.reportError(this, getProjectPropertiesFile(), message);
}
EclipseProject libraryPrj = getProject(myImporter, libraryDir);
myDirectLibraries.add(libraryPrj);
}
}
private void initLibrary(@NonNull Properties properties) throws IOException {
// This initialization must run after we've initialized the set of library
// projects so we know whether or not we're including/merging manifests
assert myDirectLibraries != null;
String value = properties.getProperty(ANDROID_LIBRARY);
myLibrary = VALUE_TRUE.equals(value);
if (!myLibrary) {
boolean mergeManifests = VALUE_TRUE.equals(properties.getProperty("manifestmerger.enabled")); //$NON-NLS-1$
if (!mergeManifests) {
// See if we (transitively) depend on libraries, and if any of them are
// android library projects with non-empty manifests
for (EclipseProject library : getAllLibraries()) {
if (library.isAndroidProject() && library.isLibrary() &&
library.getManifestFile().exists() &&
library.getManifestDoc().getDocumentElement() != null &&
XmlUtils.hasElementChildren(library.getManifestDoc().
getDocumentElement())) {
myImporter.getSummary().reportManifestsMayDiffer();
break;
}
}
}
}
}
private void initPackage() throws IOException {
myPackage = getManifestDoc().getDocumentElement().getAttribute(ATTR_PACKAGE);
}
private void initMinSdkVersion() throws IOException {
NodeList usesSdks = getManifestDoc().getDocumentElement().getElementsByTagName(NODE_USES_SDK);
if (usesSdks.getLength() > 0) {
Element usesSdk = (Element)usesSdks.item(0);
myMinSdkVersion = getApiVersion(usesSdk, ATTRIBUTE_MIN_SDK_VERSION, AndroidVersion.DEFAULT);
myTargetSdkVersion = getApiVersion(usesSdk, ATTRIBUTE_TARGET_SDK_VERSION, myMinSdkVersion);
}
else {
myMinSdkVersion = null;
myTargetSdkVersion = null;
}
}
private void initProjectName() throws IOException {
Document document = getProjectDocument();
if (document == null) {
return;
}
NodeList names = document.getElementsByTagName("name");
for (int i = 0; i < names.getLength(); i++) {
Node element = names.item(i);
myName = getStringValue((Element)element);
//noinspection VariableNotUsedInsideIf
if (myName != null) {
break;
}
}
if (myName == null) {
myName = myDir.getName();
}
}
private void initJni() throws IOException {
File jniDir = new File(myDir, "jni");
if (!jniDir.exists()) {
return;
}
//noinspection SpellCheckingInspection
if (myNdkProject) {
myNativeSources = jniDir;
File makefile = new File(jniDir, "Android.mk");
if (makefile.exists()) {
Pattern pattern = Pattern.compile("\\s*LOCAL_MODULE\\s*:=\\s*(\\S+)\\s*");
for (String line : Files.readLines(makefile, Charsets.UTF_8)) {
Matcher matcher = pattern.matcher(line);
if (matcher.matches()) {
myNativeModuleName = matcher.group(1);
if (myNativeLibs != null) {
// Remove libs from the libs/<abi> folder if they are just
// outputs from these sources
String libName = "lib" + myNativeModuleName + ".so";
ListIterator<File> iterator = myNativeLibs.listIterator();
while (iterator.hasNext()) {
File lib = iterator.next();
if (libName.equals(lib.getName())) {
iterator.remove();
}
}
if (myNativeLibs.isEmpty()) {
myNativeLibs = null;
}
}
break;
}
}
}
}
}
private void initEncoding() throws IOException {
File propertyFile = new File(myDir, ".settings" + separator +
"org.eclipse.core.resources.prefs");
if (propertyFile.exists()) {
Properties properties = PropertiesUtil.getProperties(propertyFile);
if (properties != null) {
Enumeration<?> enumeration = properties.propertyNames();
String prefix = "encoding/";
while (enumeration.hasMoreElements()) {
Object next = enumeration.nextElement();
if (next instanceof String) {
String key = (String)next;
if (key.startsWith(prefix)) {
String path = key.substring(prefix.length());
String encoding = properties.getProperty(key);
if (encoding != null && !encoding.isEmpty()) {
try {
Charset charset = Charset.forName(encoding);
if ("<project>".equals(path)) {
myDefaultCharset = charset;
}
else {
if (myFileCharsets == null) {
myFileCharsets = Maps.newHashMap();
}
File file = resolveVariableExpression(path);
if (file != null) {
myFileCharsets.put(file, charset);
}
else {
myFileCharsets.put(new File(path), charset);
}
}
}
catch (UnsupportedCharsetException uce) {
myImporter.reportWarning(this, propertyFile, "Unknown charset " + encoding);
}
}
}
}
}
}
}
}
@Nullable
Charset getProjectEncoding() {
return myDefaultCharset;
}
@Nullable
Charset getFileEncoding(@NonNull File file) {
return myFileCharsets != null ? myFileCharsets.get(file) : null;
}
private void initInstrumentation() throws IOException {
// Find unit test projects pointing to this Gradle project. Where do we look?
// For now, in direct sub directories of the project, as well as sibling directories
File projectDir = findInstrumentationTests(myDir);
if (projectDir == null && myDir.getParentFile() != null) {
projectDir = findInstrumentationTests(myDir.getParentFile());
}
if (projectDir != null && !projectDir.equals(myDir)) {
myInstrumentationDir = projectDir;
File libs = new File(myInstrumentationDir, LIBS_FOLDER);
if (libs.exists()) {
File[] files = libs.listFiles();
if (files != null) {
for (File file : files) {
if (file.isFile() && endsWithIgnoreCase(file.getPath(), DOT_JAR)) {
if (myInstrumentationJarPaths == null) {
myInstrumentationJarPaths = Lists.newArrayList();
}
myInstrumentationJarPaths.add(file);
}
}
}
}
}
}
@Nullable
private File findInstrumentationTests(File parent) {
File[] files = parent.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
File manifest = new File(file, ANDROID_MANIFEST_XML);
if (manifest.exists()) {
try {
String target = getInstrumentationTarget(myImporter, manifest);
if (target != null && target.equals(myPackage)) {
return file;
}
}
catch (IOException e) {
// Ignore this manifest
}
}
}
}
}
return null;
}
private void initClassPathEntries() throws IOException {
assert mySourcePaths == null && myJarPaths == null;
mySourcePaths = Lists.newArrayList();
myJarPaths = Lists.newArrayList();
Document document = null;
File classPathFile = getClassPathFile();
if (!classPathFile.exists()) {
File src = new File(myDir, FD_SOURCES);
if (src.exists()) {
mySourcePaths.add(src);
}
}
else {
document = myImporter.getXmlDocument(classPathFile, false);
}
if (document != null) {
NodeList entries = document.getElementsByTagName("classpathentry");
for (int i = 0; i < entries.getLength(); i++) {
Node entry = entries.item(i);
assert entry.getNodeType() == Node.ELEMENT_NODE;
Element element = (Element)entry;
String kind = element.getAttribute("kind");
String path = element.getAttribute("path");
if (kind.equals("var")) {
File resolved = resolveVariableExpression(path);
if (resolved != null) {
mySourcePaths.add(resolved);
}
else {
myImporter.reportWarning(this, getClassPathFile(), "Could not resolve path variable " + path);
}
}
else if (kind.equals("src") && !path.isEmpty()) {
if (!path.equals(GEN_FOLDER)) { // ignore special generated source folder
File resolved = resolveVariableExpression(path);
if (resolved != null) {
if (path.startsWith("/") && GradleImport.isEclipseProjectDir(resolved)) {
// It's pointing to another project. Just add a dependency.
EclipseProject lib = getProject(myImporter, resolved);
if (!myDirectLibraries.contains(lib)) {
myDirectLibraries.add(lib);
myAllLibraries = null; // force refresh if already consulted
}
}
else {
// It's some other source directory: just include as a source path
mySourcePaths.add(resolved);
}
}
else {
myImporter.reportWarning(this, getClassPathFile(), "Could not resolve source path " +
path +
" in project " +
getName() +
": ignored. The project may not " +
"compile if the given source path provided " +
"source code.");
}
}
}
else if (kind.equals("lib") && !path.isEmpty()) {
// Java library dependency. In Android projects we don't need these since
// we pick up the information from the project.properties file for library
// dependencies and the libs/ folder for jar files.
if (!isAndroidProject()) {
File resolved = resolveVariableExpression(path);
if (resolved != null) {
myJarPaths.add(resolved);
}
else {
myImporter.reportWarning(this, getClassPathFile(),
"Absolute path in the path entry: If outside project, may not " + "work correctly: " + path);
}
}
}
else if (kind.equals("output") && !path.isEmpty()) {
String relative = path.replace('/', separatorChar);
File file = new File(relative);
if (!file.isAbsolute()) {
myOutputDir = file;
}
}
// else: ignore kind="con"
}
}
// Automatically add in libraries in libs
File[] libs = new File(myDir, LIBS_FOLDER).listFiles();
if (libs != null) {
for (File lib : libs) {
if (!lib.isFile()) {
// ABI folder?
File[] libraries = lib.listFiles();
if (libraries != null) {
for (File library : libraries) {
String name = library.getName();
if (library.isFile() && name.startsWith("lib") && name.contains(".so")) { // or .endsWith? Allow libfoo.so.1 ?
if (myNativeLibs == null) {
myNativeLibs = Lists.newArrayList();
}
File relative = new File(LIBS_FOLDER, lib.getName() + separator + library.getName());
myNativeLibs.add(relative);
}
}
}
continue;
}
//noinspection ConstantConditions
assert lib.isFile();
if (!endsWithIgnoreCase(lib.getPath(), DOT_JAR)) {
continue;
}
File relative = new File(LIBS_FOLDER, lib.getName());
if (!(myJarPaths.contains(relative) || myJarPaths.contains(lib))) {
// Skip jars that are the result of a library project dependency
boolean isLibraryJar = false;
for (EclipseProject project : getAllLibraries()) {
if (!project.isAndroidProject()) {
continue;
}
String pkg = project.getPackage();
if (pkg != null) {
String jarName = pkg.replace('.', '-') + DOT_JAR;
if (jarName.equals(lib.getName())) {
isLibraryJar = true;
break;
}
}
}
if (!isLibraryJar) {
myJarPaths.add(relative);
}
}
}
Collections.sort(myJarPaths);
if (myNativeLibs != null) {
Collections.sort(myNativeLibs);
}
}
}
private Map<String, String> getProjectVariableMap() {
if (myProjectVariableMap == null) {
myProjectVariableMap = Maps.newHashMap();
Document document;
try {
document = getProjectDocument();
if (document == null) {
return myProjectVariableMap;
}
}
catch (IOException e) {
return myProjectVariableMap;
}
NodeList variables = document.getElementsByTagName("variable");
for (int i = 0, n = variables.getLength(); i < n; i++) {
Element variable = (Element)variables.item(i);
NodeList names = variable.getElementsByTagName("name");
NodeList values = variable.getElementsByTagName("value");
if (names.getLength() == 1 && values.getLength() == 1) {
String value = getStringValue((Element)values.item(0));
String key = getStringValue((Element)names.item(0));
myProjectVariableMap.put(key, value);
}
}
}
return myProjectVariableMap;
}
private Map<String, String> getLinkedResourceMap() {
if (myLinkedResourceMap == null) {
myLinkedResourceMap = Maps.newHashMap();
Document document;
try {
document = getProjectDocument();
if (document == null) {
return myProjectVariableMap;
}
}
catch (IOException e) {
return myLinkedResourceMap;
}
NodeList links = document.getElementsByTagName("link");
for (int i = 0, n = links.getLength(); i < n; i++) {
Element variable = (Element)links.item(i);
NodeList names = variable.getElementsByTagName("name");
NodeList values = variable.getElementsByTagName("locationURI");
if (names.getLength() == 1 && values.getLength() == 1) {
String value = getStringValue((Element)values.item(0));
String key = getStringValue((Element)names.item(0));
myLinkedResourceMap.put(key, value);
}
}
}
return myLinkedResourceMap;
}
@VisibleForTesting
@Nullable
File resolveVariableExpression(@NonNull String path) throws IOException {
File file = resolveVariableExpression(path, true, 0);
if (file != null && myImporter.getPathMap().containsKey(path)) {
myImporter.getPathMap().put(path, file);
}
return file;
}
@Nullable
private File resolveVariableExpression(@NonNull String path, boolean record, int depth) throws IOException {
if (depth > 50) { // probably cyclical definition of variables
return null;
}
if (path.equals("PROJECT_LOC")) {
return myDir;
}
else if (path.equals("PARENT_LOC")) {
return myDir.getParentFile();
}
else if (path.equals("WORKSPACE_LOC")) {
return myImporter.getEclipseWorkspace();
}
else if (path.startsWith("PARENT-")) {
Pattern pattern = Pattern.compile("PARENT-(\\d+)-(.+)");
Matcher matcher = pattern.matcher(path);
if (matcher.matches()) {
// Replace suffix a given number of times
int count = Integer.parseInt(matcher.group(1));
String target = matcher.group(2);
int index = target.indexOf('/');
if (index == -1) {
index = target.indexOf('\\');
}
String var = index == -1 ? target : target.substring(0, index);
File file = resolveVariableExpression(var, false, depth + 1);
if (file != null) {
File original = file;
for (int i = 0; i < count; i++) {
if (file == null) {
break;
}
file = file.getParentFile();
}
if (file == null) {
// Try again but with canonical files
file = original.getCanonicalFile();
for (int i = 0; i < count; i++) {
if (file == null) {
break;
}
file = file.getParentFile();
}
}
}
if (file != null && index != -1) {
file = new File(file, target.substring(index + 1));
}
return file;
}
}
// See if it's an absolute path
String filePath = path.replace('/', separatorChar);
File resolved = new File(filePath);
if (resolved.exists()) {
return resolved;
}
// See if it's a relative path
resolved = new File(myDir, filePath);
if (resolved.exists()) {
return resolved;
}
// Look up in shared path map (and record path for user editing in wizard
// if not resolvable)
// No -- this needs to be per project?? Only if it's used in multiple projects...
resolved = myImporter.getPathMap().get(path);
if (resolved != null) {
return resolved;
}
if (record) {
// Record the path expression such that the user can provide a resolution
myImporter.getPathMap().put(path, null);
}
// Workspace path?
if (path.startsWith("/")) { // It's / on Windows too
// Workspace path
resolved = myImporter.resolveWorkspacePath(this, path, record);
if (resolved != null) {
return resolved;
}
if (path.indexOf('/', 1) == -1 && path.indexOf('\\', 1) == -1) {
String name = path.substring(1);
// If we can't resolve workspace paths, try looking relative
// to the current project; dependent projects are often there
File parent = myDir.getParentFile();
if (parent != null) {
File sibling = new File(parent, name);
if (sibling.exists()) {
return sibling;
}
}
// Libraries are also often children
File child = new File(myDir, name);
if (child.exists()) {
return child;
}
}
}
else if (path.startsWith("$%7B")) {
// E.g. "<value>$%7BPARENT-2-PARENT_LOC%7D/Users</value>"
// This corresponds to {PARENT_LOC}/../../
int start = 4;
int end = path.indexOf("%7D", 4);
if (end != -1) {
String sub = path.substring(start, end);
File expression = resolveVariableExpression(sub, false, depth + 1);
if (expression != null) {
String suffix = path.substring(end + 3);
if (suffix.isEmpty()) {
return expression;
}
else {
resolved = new File(expression, suffix.replace('/', separatorChar));
if (resolved.exists()) {
return resolved;
}
}
}
}
}
else {
// Path variable?
int index = path.indexOf('/');
if (index == -1) {
index = path.indexOf('\\');
}
String var;
if (index == -1) {
var = path;
}
else {
var = path.substring(0, index);
}
Map<String, String> map = getLinkedResourceMap();
String expression = map.get(var);
if (expression == null || expression.equals(var)) {
map = getProjectVariableMap();
expression = map.get(var);
}
File file;
if (expression != null) {
if (expression.startsWith("file:")) {
file = SdkUtils.urlToFile(expression);
}
else {
file = resolveVariableExpression(expression, false, depth + 1);
}
}
else {
file = myImporter.resolvePathVariable(this, var, false);
}
if (file != null) {
if (index == -1) {
return file;
}
else {
resolved = new File(file, path.substring(index + 1));
if (resolved.exists()) {
return resolved;
}
}
}
}
return null;
}
private void initAndroidProject() throws IOException {
myAndroidProject = hasNature("com.android.ide.eclipse.adt.AndroidNature");
if (!myAndroidProject && getProjectDocument() == null) {
myAndroidProject = GradleImport.isAdtProjectDir(myDir);
}
myNdkProject = myAndroidProject && (hasNature("org.eclipse.cdt.core.cnature") ||
hasNature("org.eclipse.cdt.core.ccnature") ||
new File(myDir, "jni" + separator + "Android.mk").exists());
}
private boolean hasNature(String nature) throws IOException {
Document document = getProjectDocument();
if (document != null) {
NodeList natures = document.getElementsByTagName("nature");
for (int i = 0; i < natures.getLength(); i++) {
Node element = natures.item(i);
String value = getStringValue((Element)element);
if (nature.equals(value)) {
return true;
}
}
}
return false;
}
private void initLanguageLevel() throws IOException {
if (myLanguageLevel == null) {
myLanguageLevel = DEFAULT_LANGUAGE_LEVEL; // default
File file = new File(myDir, ".settings" + separator + "org.eclipse.jdt.core.prefs");
if (file.exists()) {
Properties properties = PropertiesUtil.getProperties(file);
if (properties != null) {
String source = properties.getProperty("org.eclipse.jdt.core.compiler.source");
if (source != null) {
myLanguageLevel = source;
}
}
}
}
}
private void initProguard(@NonNull Properties properties) {
myLocalProguardFiles = Lists.newArrayList();
mySdkProguardFiles = Lists.newArrayList();
String proguardConfig = properties.getProperty(PROGUARD_CONFIG);
if (proguardConfig != null && !proguardConfig.isEmpty()) {
// Be tolerant with respect to file and path separators just like
// Ant is. Allow "/" in the property file to mean whatever the file
// separator character is:
if (File.separatorChar != '/' && proguardConfig.indexOf('/') != -1) {
proguardConfig = proguardConfig.replace('/', File.separatorChar);
}
Iterable<String> paths = LintUtils.splitPath(proguardConfig);
for (String path : paths) {
if (path.startsWith(SDK_PROPERTY_REF)) {
mySdkProguardFiles.add(new File(path.substring(SDK_PROPERTY_REF.length()).replace('/', separatorChar)));
}
else if (path.startsWith(HOME_PROPERTY_REF)) {
myImporter.getSummary().reportIgnoredUserHomeProGuardFile(path);
}
else {
File proguardConfigFile = new File(path.replace('/', separatorChar));
if (!proguardConfigFile.isAbsolute()) {
proguardConfigFile = new File(myDir, proguardConfigFile.getPath());
}
if (proguardConfigFile.isFile()) {
myLocalProguardFiles.add(proguardConfigFile);
}
}
}
}
}
@NonNull
public File getDir() {
return myDir;
}
@NonNull
public File getCanonicalDir() {
return myCanonicalDir;
}
public boolean isLibrary() {
return myLibrary;
}
@NonNull
public List<File> getLocalProguardFiles() {
assert isAndroidProject();
return myLocalProguardFiles;
}
@NonNull
public List<File> getSdkProguardFiles() {
assert isAndroidProject();
return mySdkProguardFiles;
}
@NonNull
public File getResourceDir() {
assert isAndroidProject();
return new File(myDir, FD_RES);
}
@NonNull
public File getAssetsDir() {
assert isAndroidProject();
return new File(myDir, FD_ASSETS);
}
@NonNull
private File getClassPathFile() {
return new File(myDir, GradleImport.ECLIPSE_DOT_CLASSPATH);
}
@NonNull
public Document getManifestDoc() throws IOException {
assert isAndroidProject();
if (myManifestDoc == null) {
File file = getManifestFile();
myManifestDoc = myImporter.getXmlDocument(file, true);
}
return myManifestDoc;
}
@NonNull
File getManifestFile() {
assert isAndroidProject();
return new File(myDir, ANDROID_MANIFEST_XML);
}
@Nullable
public Properties getProjectProperties() throws IOException {
if (myProjectProperties == null) {
assert isAndroidProject();
File file = getProjectPropertiesFile();
if (file.exists()) {
myProjectProperties = PropertiesUtil.getProperties(file);
}
else {
myProjectProperties = new Properties();
}
}
return myProjectProperties;
}
private File getProjectPropertiesFile() {
return new File(myDir, FN_PROJECT_PROPERTIES);
}
@Nullable
private Document getProjectDocument() throws IOException {
if (myProjectDoc == null) {
File file = new File(myDir, GradleImport.ECLIPSE_DOT_PROJECT);
if (file.exists()) {
myProjectDoc = myImporter.getXmlDocument(file, false);
}
}
return myProjectDoc;
}
public boolean isAndroidProject() {
return myAndroidProject;
}
public boolean isNdkProject() {
return myNdkProject;
}
@Nullable
public File getInstrumentationDir() {
return myInstrumentationDir;
}
@Nullable
public String getPackage() {
assert isAndroidProject();
return myPackage;
}
@NonNull
public List<File> getSourcePaths() {
return mySourcePaths;
}
@NonNull
public List<String> getInferredLibraries() {
return myInferredLibraries == null ? Collections.<String>emptyList() : myInferredLibraries;
}
@NonNull
public List<File> getJarPaths() {
return myJarPaths;
}
@NonNull
public List<File> getTestJarPaths() {
return myInstrumentationJarPaths != null ? myInstrumentationJarPaths : Collections.<File>emptyList();
}
@NonNull
public List<File> getNativeLibs() {
return myNativeLibs != null ? myNativeLibs : Collections.<File>emptyList();
}
@Nullable
public File getNativeSources() {
return myNativeSources;
}
@Nullable
public String getNativeModuleName() {
return myNativeModuleName;
}
@Nullable
public File getOutputDir() {
return myOutputDir;
}
/**
* Returns "1.6", "1.7", etc
*/
@NonNull
public String getLanguageLevel() {
return myLanguageLevel;
}
@NonNull
public String getName() {
return myName != null ? myName : myDir.getName();
}
@NonNull
public AndroidVersion getMinSdkVersion() {
assert isAndroidProject();
return myMinSdkVersion != null ? myMinSdkVersion : AndroidVersion.DEFAULT;
}
@NonNull
public AndroidVersion getTargetSdkVersion() {
assert isAndroidProject();
return myTargetSdkVersion != null ? myTargetSdkVersion : getMinSdkVersion();
}
@NonNull
public AndroidVersion getCompileSdkVersion() {
assert isAndroidProject();
return myVersion == null ? new AndroidVersion(GradleImport.CURRENT_COMPILE_VERSION, null) : myVersion;
}
@Nullable
public String getAddOn() {
assert isAndroidProject();
return myAddOn;
}
@NonNull
public List<EclipseProject> getDirectLibraries() {
return myDirectLibraries;
}
@NonNull
public List<EclipseProject> getAllLibraries() {
if (myAllLibraries == null) {
if (myDirectLibraries.isEmpty()) {
return myDirectLibraries;
}
List<EclipseProject> all = new ArrayList<EclipseProject>();
Set<EclipseProject> seen = Sets.newHashSet();
Set<EclipseProject> path = Sets.newHashSet();
seen.add(this);
path.add(this);
addLibraryProjects(all, seen, path);
myAllLibraries = all;
}
return myAllLibraries;
}
private void addLibraryProjects(@NonNull Collection<EclipseProject> collection,
@NonNull Set<EclipseProject> seen,
@NonNull Set<EclipseProject> path) {
for (EclipseProject library : myDirectLibraries) {
if (seen.contains(library)) {
if (path.contains(library)) {
myImporter.reportWarning(library, library.getDir(), "Internal error: cyclic library dependency for " + library);
}
continue;
}
collection.add(library);
seen.add(library);
path.add(library);
// Recurse
library.addLibraryProjects(collection, seen, path);
path.remove(library);
}
}
@Override
public int compareTo(@NonNull EclipseProject other) {
return myDir.compareTo(other.myDir);
}
@Override
public String toString() {
return myDir.getPath();
}
@Nullable
public EclipseImportModule getModule() {
return myModule;
}
public void setModule(@Nullable EclipseImportModule module) {
myModule = module;
}
}