/*
* 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;
import com.android.tools.idea.gradle.AndroidProjectKeys;
import com.android.tools.idea.gradle.GradleSyncState;
import com.android.tools.idea.gradle.IdeaAndroidProject;
import com.android.tools.idea.gradle.compiler.PostProjectBuildTasksExecutor;
import com.android.tools.idea.gradle.customizer.ModuleCustomizer;
import com.android.tools.idea.gradle.customizer.android.*;
import com.android.tools.idea.gradle.messages.Message;
import com.android.tools.idea.gradle.messages.ProjectSyncMessages;
import com.android.tools.idea.gradle.project.PostProjectSetupTasksExecutor;
import com.android.tools.idea.sdk.DefaultSdks;
import com.android.tools.idea.sdk.Jdks;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import com.intellij.ide.impl.NewProjectUtil;
import com.intellij.openapi.application.RunResult;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.externalSystem.model.DataNode;
import com.intellij.openapi.externalSystem.model.Key;
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.externalSystem.service.project.manage.ProjectDataService;
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.ProjectRootManager;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.pom.java.LanguageLevel;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.gradle.util.GradleConstants;
import java.io.File;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import static com.android.tools.idea.gradle.messages.CommonMessageGroupNames.EXTRA_GENERATED_SOURCES;
/**
* Service that sets an Android SDK and facets to the modules of a project that has been imported from an Android-Gradle project.
*/
public class AndroidProjectDataService implements ProjectDataService<IdeaAndroidProject, Void> {
private static final Logger LOG = Logger.getInstance(AndroidProjectDataService.class);
private final List<ModuleCustomizer<IdeaAndroidProject>> myCustomizers;
// This constructor is called by the IDE. See this module's plugin.xml file, implementation of extension 'externalProjectDataService'.
public AndroidProjectDataService() {
this(ImmutableList.of(new AndroidSdkModuleCustomizer(), new AndroidFacetModuleCustomizer(), new ContentRootModuleCustomizer(),
new RunConfigModuleCustomizer(), new DependenciesModuleCustomizer(), new CompilerOutputModuleCustomizer()));
}
@VisibleForTesting
AndroidProjectDataService(@NotNull List<ModuleCustomizer<IdeaAndroidProject>> customizers) {
myCustomizers = customizers;
}
@NotNull
@Override
public Key<IdeaAndroidProject> getTargetDataKey() {
return AndroidProjectKeys.IDE_ANDROID_PROJECT;
}
/**
* Sets an Android SDK and facets to the modules of a project that has been imported from an Android-Gradle project.
*
* @param toImport contains the Android-Gradle project.
* @param project IDEA project to configure.
* @param synchronous indicates whether this operation is synchronous.
*/
@Override
public void importData(@NotNull Collection<DataNode<IdeaAndroidProject>> toImport, @NotNull Project project, boolean synchronous) {
if (!toImport.isEmpty()) {
try {
doImport(toImport, project);
}
catch (Throwable e) {
LOG.error(String.format("Failed to set up Android modules in project '%1$s'", project.getName()), e);
String msg = e.getMessage();
if (msg == null) {
msg = e.getClass().getCanonicalName();
}
GradleSyncState.getInstance(project).syncFailed(msg);
return;
}
}
PostProjectSetupTasksExecutor.getInstance(project).onProjectSyncCompletion();
}
private void doImport(final Collection<DataNode<IdeaAndroidProject>> toImport, final Project project) throws Throwable {
RunResult result = new WriteCommandAction.Simple(project) {
@Override
protected void run() throws Throwable {
LanguageLevel javaLangVersion = null;
ProjectSyncMessages messages = ProjectSyncMessages.getInstance(project);
boolean hasExtraGeneratedFolders = false;
Map<String, IdeaAndroidProject> androidProjectsByModuleName = indexByModuleName(toImport);
ModuleManager moduleManager = ModuleManager.getInstance(project);
for (Module module : moduleManager.getModules()) {
IdeaAndroidProject androidProject = androidProjectsByModuleName.get(module.getName());
customizeModule(module, project, androidProject);
if (androidProject != null) {
if (javaLangVersion == null) {
javaLangVersion = androidProject.getJavaLanguageLevel();
}
// Warn users that there are generated source folders at the wrong location.
File[] sourceFolders = androidProject.getExtraGeneratedSourceFolders();
if (sourceFolders.length > 0) {
hasExtraGeneratedFolders = true;
}
for (File folder : sourceFolders) {
// Have to add a word before the path, otherwise IDEA won't show it.
String[] text = {"Folder " + folder.getPath()};
messages.add(new Message(EXTRA_GENERATED_SOURCES, Message.Type.WARNING, text));
}
}
}
if (hasExtraGeneratedFolders) {
messages.add(new Message(EXTRA_GENERATED_SOURCES, Message.Type.INFO, "3rd-party Gradle plug-ins may be the cause"));
}
Sdk jdk = ProjectRootManager.getInstance(project).getProjectSdk();
if (jdk == null || !Jdks.isApplicableJdk(jdk, javaLangVersion)) {
jdk = Jdks.chooseOrCreateJavaSdk(javaLangVersion);
}
if (jdk == null) {
String title = String.format("Problems importing/refreshing Gradle project '%1$s':\n", project.getName());
LanguageLevel level = javaLangVersion != null ? javaLangVersion : LanguageLevel.JDK_1_6;
String msg = String.format("Unable to find a JDK %1$s installed.\n", level.getPresentableText());
msg += "After configuring a suitable JDK in the \"Project Structure\" dialog, sync the Gradle project again.";
NotificationData notification = new NotificationData(title, msg, NotificationCategory.ERROR, NotificationSource.PROJECT_SYNC);
ExternalSystemNotificationManager.getInstance(project).showNotification(GradleConstants.SYSTEM_ID, notification);
}
else {
String homePath = jdk.getHomePath();
if (homePath != null) {
NewProjectUtil.applyJdkToProject(project, jdk);
homePath = FileUtil.toSystemDependentName(homePath);
DefaultSdks.setDefaultJavaHome(new File(homePath));
PostProjectBuildTasksExecutor.getInstance(project).updateJavaLangLevelAfterBuild();
}
}
}
}.execute();
Throwable error = result.getThrowable();
if (error != null) {
throw error;
}
}
@NotNull
private static Map<String, IdeaAndroidProject> indexByModuleName(@NotNull Collection<DataNode<IdeaAndroidProject>> dataNodes) {
Map<String, IdeaAndroidProject> index = Maps.newHashMap();
for (DataNode<IdeaAndroidProject> d : dataNodes) {
IdeaAndroidProject androidProject = d.getData();
index.put(androidProject.getModuleName(), androidProject);
}
return index;
}
private void customizeModule(@NotNull Module module, @NotNull Project project, @Nullable IdeaAndroidProject ideaAndroidProject) {
for (ModuleCustomizer<IdeaAndroidProject> customizer : myCustomizers) {
customizer.customizeModule(module, project, ideaAndroidProject);
}
}
@Override
public void removeData(@NotNull Collection<? extends Void> toRemove, @NotNull Project project, boolean synchronous) {
}
}