blob: c36ed51bce3431226e72e2e9a9a0a5ab398a3849 [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.service.notification;
import com.android.SdkConstants;
import com.android.tools.idea.gradle.project.AndroidGradleProjectResolver;
import com.android.tools.idea.gradle.util.GradleUtil;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.io.Closeables;
import com.intellij.facet.ProjectFacetManager;
import com.intellij.notification.Notification;
import com.intellij.notification.NotificationListener;
import com.intellij.notification.NotificationType;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.externalSystem.model.ExternalSystemException;
import com.intellij.openapi.externalSystem.model.ProjectSystemId;
import com.intellij.openapi.externalSystem.service.notification.ExternalSystemNotificationExtension;
import com.intellij.openapi.externalSystem.service.notification.NotificationCategory;
import com.intellij.openapi.externalSystem.service.notification.NotificationData;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.util.SystemProperties;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.sdk.AndroidSdkType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.gradle.util.GradleConstants;
import javax.swing.event.HyperlinkEvent;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.android.tools.idea.gradle.project.ProjectImportErrorHandler.*;
public class GradleNotificationExtension implements ExternalSystemNotificationExtension {
private static final Logger LOG = Logger.getInstance(GradleNotificationExtension.class);
private static final Pattern ERROR_LOCATION_IN_FILE_PATTERN = Pattern.compile("Build file '(.*)' line: ([\\d]+)");
private static final Pattern ERROR_IN_FILE_PATTERN = Pattern.compile("Build file '(.*)'");
private static final Pattern MISSING_DEPENDENCY_PATTERN = Pattern.compile("Could not find (.*)\\.");
private static final Pattern MISSING_MATCHING_DEPENDENCY_PATTERN = Pattern.compile("Could not find any version that matches (.*)\\.");
private static final NotificationType DEFAULT_NOTIFICATION_TYPE = NotificationType.ERROR;
@NotNull
@Override
public ProjectSystemId getTargetExternalSystemId() {
return GradleConstants.SYSTEM_ID;
}
@Override
public void customize(@NotNull NotificationData notification, @NotNull Project project, @Nullable Throwable error) {
Throwable cause = error;
if (error instanceof UndeclaredThrowableException) {
cause = ((UndeclaredThrowableException)error).getUndeclaredThrowable();
if (cause instanceof InvocationTargetException) {
cause = ((InvocationTargetException)cause).getTargetException();
}
}
if (cause instanceof ExternalSystemException) {
updateNotification(notification, project, (ExternalSystemException)cause);
}
}
private static void updateNotification(@NotNull NotificationData notification,
@NotNull Project project,
@NotNull ExternalSystemException error) {
String msg = error.getMessage();
if (msg != null && !msg.isEmpty()) {
if (msg.startsWith(AndroidGradleProjectResolver.UNSUPPORTED_MODEL_VERSION_ERROR_PREFIX)) {
updateNotification(notification, project, msg, new FixGradleModelVersionHyperlink());
return;
}
if (msg.contains(FAILED_TO_PARSE_SDK_ERROR)) {
String pathOfBrokenSdk = findPathOfSdkMissingOrEmptyAddonsFolder(project);
String newMsg;
if (pathOfBrokenSdk != null) {
newMsg = String.format("The directory '%1$s', in the Android SDK at '%2$s', is either missing or empty", SdkConstants.FD_ADDONS,
pathOfBrokenSdk);
File sdkHomeDir = new File(pathOfBrokenSdk);
if (!sdkHomeDir.canWrite()) {
String format = "\n\nCurrent user (%1$s) does not have write access to the SDK directory.";
newMsg += String.format(format, SystemProperties.getUserName());
}
}
else {
newMsg = splitLines(msg).get(0);
}
updateNotification(notification, project, newMsg);
return;
}
List<String> lines = splitLines(msg);
String lastLine = lines.get(lines.size() - 1);
if (OPEN_GRADLE_SETTINGS.equals(lastLine)) {
updateNotification(notification, project, msg, new OpenGradleSettingsHyperlink());
return;
}
if (FIX_SDK_DIR_PROPERTY.equals(lastLine)) {
File file = new File(project.getBasePath(), SdkConstants.FN_LOCAL_PROPERTIES);
if (file.isFile()) {
// If we got this far, local.properties exists.
BufferedReader reader = null;
try {
//noinspection IOResourceOpenedButNotSafelyClosed
reader = new BufferedReader(new FileReader(file));
int counter = 0;
String line;
while ((line = reader.readLine()) != null) {
if (line.startsWith(SdkConstants.SDK_DIR_PROPERTY)) {
updateNotification(notification, project, msg, new OpenFileHyperlink(file.getPath(), counter));
return;
}
counter++;
}
}
catch (IOException e) {
LOG.info("Unable to read file: " + file.getPath(), e);
}
finally {
Closeables.closeQuietly(reader);
}
updateNotification(notification, project, msg, new OpenFileHyperlink(file.getPath(), 0));
return;
}
}
if (lastLine.contains(INSTALL_ANDROID_SUPPORT_REPO) ||
lastLine.contains(INSTALL_MISSING_PLATFORM) ||
lastLine.contains(INSTALL_MISSING_BUILD_TOOLS)) {
List<AndroidFacet> facets = ProjectFacetManager.getInstance(project).getFacets(AndroidFacet.ID);
if (!facets.isEmpty()) {
// We can only open SDK manager if the project has an Android facet. Android facet has a reference to the Android SDK manager.
updateNotification(notification, project, msg, new OpenAndroidSdkManagerHyperlink());
return;
}
updateNotification(notification, project, msg);
return;
}
if (lastLine.contains(FIX_GRADLE_VERSION)) {
List<NotificationHyperlink> hyperlinks = Lists.newArrayList();
File wrapperPropertiesFile = GradleUtil.findWrapperPropertiesFile(project);
if (wrapperPropertiesFile != null) {
// It is very likely that we need to fix the model version as well. Do everything in one shot.
NotificationHyperlink hyperlink = new FixGradleModelVersionHyperlink("Fix Gradle wrapper and re-import project", false);
hyperlinks.add(hyperlink);
}
hyperlinks.add(new OpenGradleSettingsHyperlink());
updateNotification(notification, project, msg, hyperlinks);
return;
}
String firstLine = lines.get(0);
if (firstLine.startsWith("Unknown host '")) {
List<NotificationHyperlink> hyperlinks = Lists.newArrayList();
NotificationHyperlink enableOfflineMode = ToggleOfflineModeHyperlink.enableOfflineMode(project);
if (enableOfflineMode != null) {
hyperlinks.add(enableOfflineMode);
}
String gradleDocsUrl = "http://www.gradle.org/docs/current/userguide/userguide_single.html#sec:accessing_the_web_via_a_proxy";
hyperlinks.add(new OpenUrlHyperlink(gradleDocsUrl, "Learn about configuring HTTP proxies in Gradle"));
updateNotification(notification, project, msg, hyperlinks);
return;
}
if (isInternetConnectionError(firstLine)) {
List<NotificationHyperlink> hyperlinks = Lists.newArrayList();
NotificationHyperlink enableOfflineMode = ToggleOfflineModeHyperlink.enableOfflineMode(project);
if (enableOfflineMode != null) {
hyperlinks.add(enableOfflineMode);
}
updateNotification(notification, project, msg, hyperlinks);
return;
}
if (firstLine.startsWith("No cached version of ") && firstLine.contains("available for offline mode.")) {
List<NotificationHyperlink> hyperlinks = Lists.newArrayList();
NotificationHyperlink disableOfflineMode = ToggleOfflineModeHyperlink.disableOfflineMode(project);
if (disableOfflineMode != null) {
hyperlinks.add(disableOfflineMode);
}
updateNotification(notification, project, firstLine, hyperlinks);
return;
}
Matcher matcher = MISSING_MATCHING_DEPENDENCY_PATTERN.matcher(firstLine);
if (matcher.matches()) {
String dependency = matcher.group(1);
createMissingDependencyNotification(notification, project, firstLine, dependency);
return;
}
matcher = MISSING_DEPENDENCY_PATTERN.matcher(firstLine);
if (matcher.matches() && lines.size() > 1 && lines.get(1).startsWith("Required by:")) {
String dependency = matcher.group(1);
if (!Strings.isNullOrEmpty(dependency)) {
if (lastLine != null) {
Pair<String, Integer> errorLocation = getErrorLocation(lastLine);
if (errorLocation != null) {
// We have a location in file, show the "Open File" hyperlink.
String filePath = errorLocation.getFirst();
int line = errorLocation.getSecond();
createMissingDependencyNotification(notification, project, msg, dependency, new OpenFileHyperlink(filePath, line - 1));
return;
}
}
createMissingDependencyNotification(notification, project, msg, dependency);
return;
}
}
for (String line : lines) {
// This happens when Gradle cannot find the Android Gradle plug-in in Maven Central.
if (line == null) {
continue;
}
matcher = MISSING_MATCHING_DEPENDENCY_PATTERN.matcher(line);
if (matcher.matches()) {
String dependency = matcher.group(1);
createMissingDependencyNotification(notification, project, line, dependency);
return;
}
}
if (lastLine != null) {
if (lastLine.contains(UNEXPECTED_ERROR_FILE_BUG)) {
updateNotification(notification, project, msg, new FileBugHyperlink(), new ShowLogHyperlink());
return;
}
Pair<String, Integer> errorLocation = getErrorLocation(lastLine);
if (errorLocation != null) {
String filePath = errorLocation.getFirst();
int line = errorLocation.getSecond();
updateNotification(notification, project, msg, new OpenFileHyperlink(filePath, line - 1));
}
}
}
}
private static void createMissingDependencyNotification(@NotNull NotificationData notification,
@NotNull Project project,
@NotNull String msg,
@NotNull String dependency,
@NotNull NotificationHyperlink... additionalHyperlinks) {
List<NotificationHyperlink> hyperlinks = Lists.newArrayList(additionalHyperlinks);
ToggleOfflineModeHyperlink disableOfflineMode = ToggleOfflineModeHyperlink.disableOfflineMode(project);
if (disableOfflineMode != null) {
hyperlinks.add(0, disableOfflineMode);
}
hyperlinks.add(new SearchInBuildFilesHyperlink(dependency));
updateNotification(notification, project, msg, hyperlinks);
}
@Nullable
private static Pair<String, Integer> getErrorLocation(@NotNull String msg) {
Matcher matcher = ERROR_LOCATION_IN_FILE_PATTERN.matcher(msg);
if (matcher.matches()) {
String filePath = matcher.group(1);
int line = -1;
try {
line = Integer.parseInt(matcher.group(2));
}
catch (NumberFormatException e) {
// ignored.
}
return Pair.create(filePath, line);
}
matcher = ERROR_IN_FILE_PATTERN.matcher(msg);
if (matcher.matches()) {
String filePath = matcher.group(1);
return Pair.create(filePath, -1);
}
return null;
}
@Nullable
private static String findPathOfSdkMissingOrEmptyAddonsFolder(@NotNull Project project) {
ModuleManager moduleManager = ModuleManager.getInstance(project);
for (Module module : moduleManager.getModules()) {
Sdk moduleSdk = ModuleRootManager.getInstance(module).getSdk();
if (moduleSdk != null && moduleSdk.getSdkType().equals(AndroidSdkType.getInstance())) {
String sdkHomeDirPath = moduleSdk.getHomePath();
File addonsDir = new File(sdkHomeDirPath, SdkConstants.FD_ADDONS);
if (!addonsDir.isDirectory() || FileUtil.notNullize(addonsDir.listFiles()).length == 0) {
return sdkHomeDirPath;
}
}
}
return null;
}
@NotNull
private static List<String> splitLines(@NotNull String s) {
return Lists.newArrayList(Splitter.on('\n').split(s));
}
private static void updateNotification(@NotNull NotificationData notification,
@NotNull Project project,
@NotNull String errorMsg,
@NotNull List<NotificationHyperlink> hyperlinks) {
updateNotification(notification, project, errorMsg, hyperlinks.toArray(new NotificationHyperlink[hyperlinks.size()]));
}
private static boolean isInternetConnectionError(@NotNull String msg) {
return msg.startsWith("Could not GET ") || msg.startsWith("Could not HEAD ") || msg.startsWith("Network is unreachable");
}
@VisibleForTesting
static void updateNotification(@NotNull NotificationData notification,
@NotNull final Project project,
@NotNull String errorMsg,
@NotNull NotificationHyperlink... hyperlinks) {
String text = errorMsg;
int hyperlinkCount = hyperlinks.length;
if (hyperlinkCount > 0) {
StringBuilder b = new StringBuilder();
for (int i = 0; i < hyperlinkCount; i++) {
b.append(hyperlinks[i].toString());
if (i < hyperlinkCount - 1) {
b.append(" ");
}
}
text += ('\n' + b.toString());
}
String title = String.format("Failed to refresh Gradle project '%1$s'", project.getName());
notification.setTitle(title);
notification.setMessage(text);
notification.setNotificationCategory(NotificationCategory.convert(DEFAULT_NOTIFICATION_TYPE));
for (final NotificationHyperlink hyperlink : hyperlinks) {
notification.setListener(hyperlink.getUrl(), new NotificationListener.Adapter() {
@Override
protected void hyperlinkActivated(@NotNull Notification notification, @NotNull HyperlinkEvent e) {
hyperlink.executeIfClicked(project, e);
}
});
}
}
}