blob: 90f41c53bdc11de3cb4cc3f34bf04bc42c0b9ab8 [file] [log] [blame]
/*
* Copyright (C) 2014 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.messages;
import com.android.builder.model.SyncIssue;
import com.android.ide.common.repository.GradleCoordinate;
import com.android.ide.common.repository.SdkMavenRepository;
import com.android.sdklib.repository.descriptors.IPkgDesc;
import com.android.tools.idea.gradle.IdeaGradleProject;
import com.android.tools.idea.gradle.dependency.DependencySetupErrors;
import com.android.tools.idea.gradle.dependency.DependencySetupErrors.MissingModule;
import com.android.tools.idea.gradle.facet.AndroidGradleFacet;
import com.android.tools.idea.gradle.project.GradleProjectImporter;
import com.android.tools.idea.gradle.project.compatibility.VersionCompatibilityService;
import com.android.tools.idea.gradle.project.compatibility.VersionCompatibilityService.VersionIncompatibilityMessage;
import com.android.tools.idea.gradle.project.subset.ProjectSubset;
import com.android.tools.idea.gradle.service.notification.hyperlink.NotificationHyperlink;
import com.android.tools.idea.gradle.service.notification.hyperlink.OpenFileHyperlink;
import com.android.tools.idea.sdk.wizard.SdkQuickfixWizard;
import com.android.tools.idea.startup.AndroidStudioSpecificInitializer;
import com.android.tools.idea.structure.gradle.AndroidProjectSettingsService;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.externalSystem.service.notification.ExternalSystemNotificationManager;
import com.intellij.openapi.externalSystem.service.notification.NotificationCategory;
import com.intellij.openapi.externalSystem.service.notification.NotificationData;
import com.intellij.openapi.externalSystem.service.notification.NotificationSource;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ui.configuration.ProjectSettingsService;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.pom.Navigatable;
import com.intellij.pom.NonNavigatable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.gradle.settings.GradleSettings;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import static com.android.tools.idea.gradle.messages.CommonMessageGroupNames.*;
import static com.android.tools.idea.gradle.service.notification.errors.AbstractSyncErrorHandler.updateNotification;
import static com.android.tools.idea.gradle.util.GradleUtil.GRADLE_SYSTEM_ID;
import static com.android.tools.idea.gradle.util.GradleUtil.findDependency;
import static com.android.tools.idea.gradle.util.Projects.*;
import static com.intellij.openapi.externalSystem.service.notification.NotificationSource.PROJECT_SYNC;
import static com.intellij.openapi.util.text.StringUtil.isNotEmpty;
import static com.intellij.openapi.util.text.StringUtil.join;
import static com.intellij.openapi.vfs.VfsUtilCore.virtualToIoFile;
import static com.intellij.util.ArrayUtil.toStringArray;
/**
* Service that collects and displays, in the "Messages" tool window, post-sync project setup messages (errors, warnings, etc.)
*/
public class ProjectSyncMessages {
private static final NotificationSource NOTIFICATION_SOURCE = PROJECT_SYNC;
@NotNull private final Project myProject;
@NotNull private final ExternalSystemNotificationManager myNotificationManager;
@NotNull
public static ProjectSyncMessages getInstance(@NotNull Project project) {
return ServiceManager.getService(project, ProjectSyncMessages.class);
}
public int getErrorCount() {
return myNotificationManager.getMessageCount(null, NOTIFICATION_SOURCE, NotificationCategory.ERROR, GRADLE_SYSTEM_ID);
}
public ProjectSyncMessages(@NotNull Project project, @NotNull ExternalSystemNotificationManager manager) {
myProject = project;
myNotificationManager = manager;
}
public int getMessageCount(@NotNull String groupName) {
return myNotificationManager.getMessageCount(groupName, NOTIFICATION_SOURCE, null, GRADLE_SYSTEM_ID);
}
public boolean isEmpty() {
return myNotificationManager.getMessageCount(NOTIFICATION_SOURCE, null, GRADLE_SYSTEM_ID) == 0;
}
public void reportComponentIncompatibilities() {
VersionCompatibilityService compatibilityService = VersionCompatibilityService.getInstance();
List<VersionIncompatibilityMessage> messages = compatibilityService.checkComponentCompatibility(myProject);
for (VersionIncompatibilityMessage message : messages) {
add(message.getMessage(), message.getQuickFixes());
}
if (!messages.isEmpty()) {
setHasSyncErrors(myProject, true);
}
}
public void reportSyncIssues(@NotNull Collection<SyncIssue> syncIssues, @NotNull Module module) {
if (syncIssues.isEmpty()) {
return;
}
boolean hasSyncErrors = false;
VirtualFile buildFile = getBuildFile(module);
for (SyncIssue syncIssue : syncIssues) {
if (syncIssue.getSeverity() == SyncIssue.SEVERITY_ERROR) {
hasSyncErrors = true;
}
switch (syncIssue.getType()) {
case SyncIssue.TYPE_UNRESOLVED_DEPENDENCY:
reportUnresolvedDependency(syncIssue.getData(), module, buildFile);
break;
default:
String group = UNHANDLED_SYNC_ISSUE_TYPE;
String text = syncIssue.getMessage();
Message.Type severity = syncIssue.getType() == SyncIssue.SEVERITY_ERROR ? Message.Type.ERROR : Message.Type.WARNING;
Message msg;
if (buildFile != null) {
msg = new Message(module.getProject(), group, severity, buildFile, -1, -1, text);
}
else {
msg = new Message(group, severity, NonNavigatable.INSTANCE, text);
}
add(msg);
}
}
if (hasSyncErrors) {
setHasSyncErrors(myProject, true);
}
}
public void reportUnresolvedDependencies(@NotNull Collection<String> unresolvedDependencies, @NotNull Module module) {
if (unresolvedDependencies.isEmpty()) {
return;
}
VirtualFile buildFile = getBuildFile(module);
for (String dep : unresolvedDependencies) {
reportUnresolvedDependency(dep, module, buildFile);
}
setHasSyncErrors(myProject, true);
}
@Nullable
private static VirtualFile getBuildFile(@NotNull Module module) {
AndroidGradleFacet gradleFacet = AndroidGradleFacet.getInstance(module);
if (gradleFacet != null && gradleFacet.getGradleProject() != null) {
IdeaGradleProject gradleProject = gradleFacet.getGradleProject();
return gradleProject.getBuildFile();
}
return null;
}
private void reportUnresolvedDependency(@NotNull String dependency, @NotNull Module module, @Nullable VirtualFile buildFile) {
List<NotificationHyperlink> hyperlinks = Lists.newArrayList();
String group;
if (dependency.startsWith("com.android.support")) {
group = UNRESOLVED_ANDROID_DEPENDENCIES;
hyperlinks.add(new InstallRepositoryHyperlink(SdkMavenRepository.ANDROID));
}
else if (dependency.startsWith("com.google.android")) {
group = UNRESOLVED_ANDROID_DEPENDENCIES;
hyperlinks.add(new InstallRepositoryHyperlink(SdkMavenRepository.GOOGLE));
}
else {
group = UNRESOLVED_DEPENDENCIES;
if (isOfflineBuildModeEnabled(myProject)) {
NotificationHyperlink disableOfflineModeHyperlink = new NotificationHyperlink("disable.gradle.offline.mode", "Disable offline mode and Sync") {
@Override
protected void execute(@NotNull Project project) {
GradleSettings.getInstance(myProject).setOfflineWork(false);
GradleProjectImporter.getInstance().requestProjectSync(project, null);
}
};
hyperlinks.add(disableOfflineModeHyperlink);
}
}
String text = "Failed to resolve: " + dependency;
Message msg;
if (buildFile != null) {
int lineNumber = -1;
int column = -1;
Document document = FileDocumentManager.getInstance().getDocument(buildFile);
if (document != null) {
TextRange textRange = findDependency(dependency, document.getText());
if (textRange != null) {
lineNumber = document.getLineNumber(textRange.getStartOffset());
if (lineNumber > -1) {
int lineStartOffset = document.getLineStartOffset(lineNumber);
column = textRange.getStartOffset() - lineStartOffset;
}
}
}
msg = new Message(module.getProject(), group, Message.Type.ERROR, buildFile, lineNumber, column, text);
String hyperlinkText = lineNumber > -1 ? "Show in File": "Open File";
hyperlinks.add(new OpenFileHyperlink(buildFile.getPath(), hyperlinkText, lineNumber, column));
}
else {
msg = new Message(group, Message.Type.ERROR, NonNavigatable.INSTANCE, text);
}
if (AndroidStudioSpecificInitializer.isAndroidStudio()) {
GradleCoordinate coordinate = GradleCoordinate.parseCoordinateString(dependency);
if (coordinate != null) {
hyperlinks.add(new ShowDependencyInProjectStructureHyperlink(module, coordinate));
}
}
add(msg, hyperlinks.toArray(new NotificationHyperlink[hyperlinks.size()]));
}
public void reportDependencySetupErrors() {
DependencySetupErrors setupErrors = getDependencySetupErrors(myProject);
if (setupErrors != null) {
reportModulesNotFoundErrors(setupErrors);
setDependencySetupErrors(myProject, null);
}
}
private void reportModulesNotFoundErrors(@NotNull DependencySetupErrors setupErrors) {
reportModulesNotFoundIssues(MISSING_DEPENDENCIES_BETWEEN_MODULES, setupErrors.getMissingModules());
for (String dependent : setupErrors.getMissingNames()) {
String msg = String.format("Module '%1$s' depends on modules that do not have a name.", dependent);
add(new Message(FAILED_TO_SET_UP_DEPENDENCIES, Message.Type.ERROR, msg));
}
for (String dependent : setupErrors.getDependentsOnLibrariesWithoutBinaryPath()) {
String msg = String.format("Module '%1$s' depends on libraries that do not have a 'binary' path.", dependent);
add(new Message(FAILED_TO_SET_UP_DEPENDENCIES, Message.Type.ERROR, msg));
}
for (DependencySetupErrors.InvalidModuleDependency dependency : setupErrors.getInvalidModuleDependencies()) {
String msg = String.format("Ignoring dependency of module '%1$s' on module '%2$s'. %3$s",
dependency.dependent, dependency.dependency.getName(), dependency.detail);
VirtualFile buildFile = getBuildFile(dependency.dependency);
assert buildFile != null;
add(new Message(FAILED_TO_SET_UP_DEPENDENCIES, Message.Type.WARNING, new OpenFileDescriptor(dependency.dependency.getProject(), buildFile, 0), msg));
}
reportModulesNotFoundIssues(FAILED_TO_SET_UP_DEPENDENCIES, setupErrors.getMissingModulesWithBackupLibraries());
}
private void reportModulesNotFoundIssues(@NotNull String groupName, @NotNull List<MissingModule> missingModules) {
if (!missingModules.isEmpty()) {
Message.Type severity = Message.Type.ERROR;
for (MissingModule missingModule : missingModules) {
List<String> messageLines = Lists.newArrayList();
StringBuilder text = new StringBuilder();
text.append(String.format("Unable to find module with Gradle path '%1$s' (needed by module", missingModule.dependencyPath));
addDependentsToText(text, missingModule.dependentNames);
text.append(".)");
messageLines.add(text.toString());
String backupLibraryName = missingModule.backupLibraryName;
if (isNotEmpty(backupLibraryName)) {
severity = Message.Type.WARNING;
String msg = String.format("Linking to library '%1$s' instead.", backupLibraryName);
messageLines.add(msg);
}
add(new Message(groupName, severity, toStringArray(messageLines)));
}
// If the project is really a subset of the project, attempt to find and include missing modules.
ProjectSubset projectSubset = ProjectSubset.getInstance(myProject);
String[] selection = projectSubset.getSelection();
boolean hasSelection = selection != null && selection.length > 0;
if (severity == Message.Type.ERROR && hasSelection && projectSubset.hasCachedModules()) {
String msg = "The missing modules may have been excluded from the project subset.";
add(new Message(groupName, Message.Type.INFO, msg), new IncludeMissingModulesHyperlink(missingModules));
}
}
}
private static void addDependentsToText(@NotNull StringBuilder text, @NotNull List<String> dependents) {
assert !dependents.isEmpty();
if (dependents.size() == 1) {
text.append(String.format(" '%1$s'", dependents.get(0)));
return;
}
text.append("s: ");
int i = 0;
for (String dependent : dependents) {
if (i++ > 0) {
text.append(", ");
}
text.append(String.format("'%1$s'", dependent));
}
}
public void add(@NotNull final Message message, @NotNull NotificationHyperlink... hyperlinks) {
Navigatable navigatable = message.getNavigatable();
String title = message.getGroupName();
String errorMsg = join(message.getText(), "\n");
VirtualFile file = message.getFile();
String filePath = file != null ? virtualToIoFile(file).getPath() : null;
NotificationCategory category = NotificationCategory.convert(message.getType().getValue());
NotificationData notification =
new NotificationData(title, errorMsg, category, NOTIFICATION_SOURCE, filePath, message.getLine(), message.getColumn(), false);
notification.setNavigatable(navigatable);
if (hyperlinks.length > 0) {
updateNotification(notification, myProject, title, errorMsg, hyperlinks);
}
myNotificationManager.showNotification(GRADLE_SYSTEM_ID, notification);
}
public void removeMessages(@NotNull String... groupNames) {
for (String groupName : groupNames) {
myNotificationManager.clearNotifications(groupName, NOTIFICATION_SOURCE, GRADLE_SYSTEM_ID);
}
}
private static class ShowDependencyInProjectStructureHyperlink extends NotificationHyperlink {
@NotNull private final Module myModule;
@NotNull private final GradleCoordinate myDependency;
ShowDependencyInProjectStructureHyperlink(@NotNull Module module, @NotNull GradleCoordinate dependency) {
super("open.dependency.in.project.structure", "Show in Project Structure dialog");
myModule = module;
myDependency = dependency;
}
@Override
protected void execute(@NotNull Project project) {
ProjectSettingsService service = ProjectSettingsService.getInstance(project);
if (service instanceof AndroidProjectSettingsService) {
((AndroidProjectSettingsService)service).openAndSelectDependency(myModule, myDependency);
}
}
}
private static class InstallRepositoryHyperlink extends NotificationHyperlink {
@NotNull private final SdkMavenRepository myRepository;
InstallRepositoryHyperlink(@NotNull SdkMavenRepository repository) {
super("install.m2.repo", "Install Repository and sync project");
myRepository = repository;
}
@Override
protected void execute(@NotNull Project project) {
List<IPkgDesc> requested = Lists.newArrayList();
requested.add(myRepository.getPackageDescription());
SdkQuickfixWizard wizard = new SdkQuickfixWizard(project, null, requested);
wizard.init();
wizard.setTitle("Install Missing Components");
if (wizard.showAndGet()) {
GradleProjectImporter.getInstance().requestProjectSync(project, null);
}
}
}
/**
* "Quick Fix" link that attempts to find and include any modules that were not previously included in the project subset.
*/
private static class IncludeMissingModulesHyperlink extends NotificationHyperlink {
@NotNull private final Set<String> myModuleGradlePaths;
IncludeMissingModulesHyperlink(@NotNull List<MissingModule> missingModules) {
super("include.missing.modules", "Find and include missing modules");
myModuleGradlePaths = Sets.newHashSetWithExpectedSize(missingModules.size());
for (MissingModule module : missingModules) {
myModuleGradlePaths.add(module.dependencyPath);
}
}
@Override
protected void execute(@NotNull Project project) {
ProjectSubset.getInstance(project).findAndIncludeModules(myModuleGradlePaths);
}
}
}