blob: 4533594e56f7beb121bf5d35795e1ea38275241d [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.project;
import com.android.SdkConstants;
import com.android.tools.idea.gradle.service.notification.errors.FailedToParseSdkErrorHandler;
import com.android.tools.idea.gradle.service.notification.errors.MissingAndroidSdkErrorHandler;
import com.google.common.annotations.VisibleForTesting;
import com.intellij.openapi.externalSystem.model.ExternalSystemException;
import com.intellij.openapi.externalSystem.model.LocationAwareExternalSystemException;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import org.gradle.tooling.UnsupportedVersionException;
import org.gradle.tooling.model.UnsupportedMethodException;
import org.gradle.tooling.provider.model.ToolingModelBuilderRegistry;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.gradle.service.project.AbstractProjectImportErrorHandler;
import java.io.File;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.android.SdkConstants.GRADLE_MINIMUM_VERSION;
/**
* Provides better error messages for android projects import failures.
*/
public class ProjectImportErrorHandler extends AbstractProjectImportErrorHandler {
public static final String GRADLE_DSL_METHOD_NOT_FOUND_ERROR_PREFIX = "Gradle DSL method not found";
public static final String CONNECTION_PERMISSION_DENIED_PREFIX = "Connection to the Internet denied.";
public static final String INSTALL_ANDROID_SUPPORT_REPO = "Please install the Android Support Repository from the Android SDK Manager.";
private static final Pattern SDK_NOT_FOUND_PATTERN = Pattern.compile("The SDK directory '(.*?)' does not exist.");
private static final Pattern CLASS_NOT_FOUND_PATTERN = Pattern.compile("(.+) not found.");
private static final String EMPTY_LINE = "\n\n";
private static final String UNSUPPORTED_GRADLE_VERSION_ERROR = "Gradle version " + GRADLE_MINIMUM_VERSION + " is required";
private static final String SDK_DIR_PROPERTY_MISSING = "No sdk.dir property defined in local.properties file.";
private static final Pattern ERROR_LOCATION_PATTERN = Pattern.compile(".* file '(.*)'( line: ([\\d]+))?");
@Override
@Nullable
public ExternalSystemException getUserFriendlyError(@NotNull Throwable error,
@NotNull String projectPath,
@Nullable String buildFilePath) {
if (error instanceof ExternalSystemException) {
// This is already a user-friendly error.
return (ExternalSystemException)error;
}
Pair<Throwable, String> rootCauseAndLocation = getRootCauseAndLocation(error);
Throwable rootCause = rootCauseAndLocation.getFirst();
if (isOldGradleVersion(rootCause)) {
String msg = "The project is using an unsupported version of Gradle.\n" + FIX_GRADLE_VERSION;
// Location of build.gradle is useless for this error. Omitting it.
return createUserFriendlyError(msg, null);
}
final String rootCauseText = rootCause.toString();
if (StringUtil.startsWith(rootCauseText, "org.gradle.api.internal.MissingMethodException")) {
String method = parseMissingMethod(rootCauseText);
return createUserFriendlyError(GRADLE_DSL_METHOD_NOT_FOUND_ERROR_PREFIX + ": '" + method + "'", rootCauseAndLocation.getSecond());
}
if (rootCause instanceof SocketException) {
String message = rootCause.getMessage();
if (message != null && message.contains("Permission denied: connect")) {
// Location of build.gradle is useless for this error. Omitting it.
return createUserFriendlyError(CONNECTION_PERMISSION_DENIED_PREFIX, null);
}
}
if (rootCause instanceof UnknownHostException) {
String msg = String.format("Unknown host '%1$s'. You may need to adjust the proxy settings in Gradle.", rootCause.getMessage());
return createUserFriendlyError(msg, null);
}
if (rootCause instanceof IllegalStateException || rootCause instanceof ExternalSystemException) {
// Missing platform in SDK now also comes as a ExternalSystemException. This may be caused by changes in IDEA's "External System
// Import" framework.
String msg = rootCause.getMessage();
if (msg != null) {
if (msg.startsWith("failed to find target android-")) {
// Location of build.gradle is useless for this error. Omitting it.
return createUserFriendlyError(msg, null);
}
if (msg.startsWith("failed to find Build Tools")) {
// Location of build.gradle is useless for this error. Omitting it.
return createUserFriendlyError(msg, null);
}
}
}
if (rootCause instanceof RuntimeException) {
String msg = rootCause.getMessage();
// With this condition we cover 2 similar messages about the same problem.
if (msg != null && msg.contains("Could not find") && msg.contains("com.android.support:")) {
// We keep the original error message and we append a hint about how to fix the missing dependency.
String newMsg = msg + EMPTY_LINE + INSTALL_ANDROID_SUPPORT_REPO;
// Location of build.gradle is useless for this error. Omitting it.
return createUserFriendlyError(newMsg, null);
}
if (msg != null && msg.contains(FailedToParseSdkErrorHandler.FAILED_TO_PARSE_SDK_ERROR)) {
String newMsg = msg + EMPTY_LINE + "The Android SDK may be missing the directory 'add-ons'.";
// Location of build.gradle is useless for this error. Omitting it.
return createUserFriendlyError(newMsg, null);
}
if (msg != null && (msg.equals(SDK_DIR_PROPERTY_MISSING) || SDK_NOT_FOUND_PATTERN.matcher(msg).matches())) {
String newMsg = msg;
File buildProperties = new File(projectPath, SdkConstants.FN_LOCAL_PROPERTIES);
if (buildProperties.isFile()) {
newMsg += EMPTY_LINE + MissingAndroidSdkErrorHandler.FIX_SDK_DIR_PROPERTY;
}
return createUserFriendlyError(newMsg, null);
}
}
if (rootCause instanceof OutOfMemoryError) {
// The OutOfMemoryError happens in the Gradle daemon process.
String originalMessage = rootCause.getMessage();
String msg = "Out of memory";
if (originalMessage != null && !originalMessage.isEmpty()) {
msg = msg + ": " + originalMessage;
}
// Location of build.gradle is useless for this error. Omitting it.
return createUserFriendlyError(msg, null);
}
if (rootCause instanceof NoSuchMethodError) {
String msg = String.format("Unable to find method '%1$s'.", rootCause.getMessage());
// Location of build.gradle is useless for this error. Omitting it.
return createUserFriendlyError(msg, null);
}
if (rootCause instanceof ClassNotFoundException) {
String className = rootCause.getMessage();
Matcher matcher = CLASS_NOT_FOUND_PATTERN.matcher(className);
if (matcher.matches()) {
className = matcher.group(1);
}
String msg = String.format("Unable to load class '%1$s'.", className);
// Location of build.gradle is useless for this error. Omitting it.
return createUserFriendlyError(msg, null);
}
// give others GradleProjectResolverExtensions a chance to handle this error
return null;
}
private static boolean isOldGradleVersion(@NotNull Throwable error) {
if (error instanceof UnsupportedVersionException) {
return true;
}
if (error instanceof UnsupportedMethodException) {
String msg = error.getMessage();
if (msg != null && msg.contains("GradleProject.getBuildScript")) {
return true;
}
}
if (error instanceof ClassNotFoundException) {
String msg = error.getMessage();
if (msg != null && msg.contains(ToolingModelBuilderRegistry.class.getName())) {
return true;
}
}
if (error instanceof RuntimeException) {
String msg = error.getMessage();
if (msg != null && msg.startsWith(UNSUPPORTED_GRADLE_VERSION_ERROR)) {
return true;
}
}
return false;
}
// The default implementation in IDEA only retrieves the location in build.gradle files. This implementation also handle location in
// settings.gradle file.
@Override
@Nullable
public String getLocationFrom(@NotNull Throwable error) {
String errorToString = error.toString();
if (errorToString.contains("LocationAwareException")) {
// LocationAwareException is never passed, but converted into a PlaceholderException that has the toString value of the original
// LocationAwareException.
String location = error.getMessage();
if (location != null && (location.startsWith("Build file '") || location.startsWith("Settings file '"))) {
// Only the first line contains the location of the error. Discard the rest.
String[] lines = StringUtil.splitByLines(location);
return lines.length > 0 ? lines[0] : null;
}
}
return null;
}
@Override
@NotNull
public ExternalSystemException createUserFriendlyError(@NotNull String msg, @Nullable String location, @NotNull String... quickFixes) {
if (!StringUtil.isEmpty(location)) {
Pair<String, Integer> pair = getErrorLocation(location);
if (pair != null) {
return new LocationAwareExternalSystemException(msg, pair.first, pair.getSecond(), quickFixes);
}
}
return new ExternalSystemException(msg, null, quickFixes);
}
@VisibleForTesting
@Nullable
static Pair<String, Integer> getErrorLocation(@NotNull String location) {
Matcher matcher = ERROR_LOCATION_PATTERN.matcher(location);
if (matcher.matches()) {
String filePath = matcher.group(1);
int line = -1;
String lineAsText = matcher.group(3);
if (lineAsText != null) {
try {
line = Integer.parseInt(lineAsText);
}
catch (NumberFormatException e) {
// ignored.
}
}
return Pair.create(filePath, line);
}
return null;
}
}