/*
* Copyright 2013-2016 Sergey Ignatov, Alexander Zolotov, Florin Patan
*
* 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.goide.project;
import com.goide.GoConstants;
import com.goide.configuration.GoLibrariesConfigurableProvider;
import com.goide.sdk.GoSdkService;
import com.goide.sdk.GoSdkUtil;
import com.goide.util.GoUtil;
import com.intellij.ProjectTopics;
import com.intellij.ide.util.PropertiesComponent;
import com.intellij.notification.Notification;
import com.intellij.notification.NotificationListener;
import com.intellij.notification.NotificationType;
import com.intellij.notification.Notifications;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleComponent;
import com.intellij.openapi.progress.ProgressIndicatorProvider;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.*;
import com.intellij.openapi.roots.impl.OrderEntryUtil;
import com.intellij.openapi.roots.impl.libraries.LibraryEx;
import com.intellij.openapi.roots.libraries.Library;
import com.intellij.openapi.roots.libraries.LibraryTable;
import com.intellij.openapi.roots.libraries.LibraryTablesRegistrar;
import com.intellij.openapi.startup.StartupManager;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.vfs.*;
import com.intellij.psi.PsiFileSystemItem;
import com.intellij.psi.search.FilenameIndex;
import com.intellij.util.Alarm;
import com.intellij.util.ThreeState;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.messages.MessageBusConnection;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.TestOnly;
import javax.swing.event.HyperlinkEvent;
import java.util.Collection;
import java.util.List;
import java.util.Set;
public class GoModuleLibrariesInitializer implements ModuleComponent {
private static final String GO_LIB_NAME = "GOPATH";
private static final String GO_LIBRARIES_NOTIFICATION_HAD_BEEN_SHOWN = "go.libraries.notification.had.been.shown";
private static final String GO_VENDORING_NOTIFICATION_HAD_BEEN_SHOWN = "go.vendoring.notification.had.been.shown";
private static final int UPDATE_DELAY = 300;
private static boolean isTestingMode;
private final Alarm myAlarm;
private final MessageBusConnection myConnection;
private boolean myModuleInitialized;
@NotNull private final Set<VirtualFile> myLastHandledGoPathSourcesRoots = ContainerUtil.newHashSet();
@NotNull private final Set<VirtualFile> myLastHandledExclusions = ContainerUtil.newHashSet();
@NotNull private final Set<LocalFileSystem.WatchRequest> myWatchedRequests = ContainerUtil.newHashSet();
@NotNull private final Module myModule;
@NotNull private final VirtualFileAdapter myFilesListener = new VirtualFileAdapter() {
@Override
public void fileCreated(@NotNull VirtualFileEvent event) {
if (GoConstants.VENDOR.equals(event.getFileName()) && event.getFile().isDirectory()) {
showVendoringNotification();
}
}
};
@TestOnly
public static void setTestingMode(@NotNull Disposable disposable) {
isTestingMode = true;
Disposer.register(disposable, () -> {
//noinspection AssignmentToStaticFieldFromInstanceMethod
isTestingMode = false;
});
}
public GoModuleLibrariesInitializer(@NotNull Module module) {
myModule = module;
myAlarm = ApplicationManager.getApplication().isUnitTestMode() ? new Alarm() : new Alarm(Alarm.ThreadToUse.POOLED_THREAD, myModule);
myConnection = myModule.getMessageBus().connect();
}
@Override
public void moduleAdded() {
if (!myModuleInitialized) {
myConnection.subscribe(ProjectTopics.PROJECT_ROOTS, new ModuleRootAdapter() {
@Override
public void rootsChanged(ModuleRootEvent event) {
scheduleUpdate();
}
});
myConnection.subscribe(GoLibrariesService.LIBRARIES_TOPIC, newRootUrls -> scheduleUpdate());
Project project = myModule.getProject();
StartupManager.getInstance(project).runWhenProjectIsInitialized(() -> {
if (!project.isDisposed() && !myModule.isDisposed()) {
for (PsiFileSystemItem vendor : FilenameIndex.getFilesByName(project, GoConstants.VENDOR, GoUtil.moduleScope(myModule), true)) {
if (vendor.isDirectory()) {
showVendoringNotification();
break;
}
}
}
});
VirtualFileManager.getInstance().addVirtualFileListener(myFilesListener);
}
scheduleUpdate(0);
myModuleInitialized = true;
}
private void scheduleUpdate() {
scheduleUpdate(UPDATE_DELAY);
}
private void scheduleUpdate(int delay) {
myAlarm.cancelAllRequests();
UpdateRequest updateRequest = new UpdateRequest();
if (isTestingMode) {
ApplicationManager.getApplication().invokeLater(updateRequest);
}
else {
myAlarm.addRequest(updateRequest, delay);
}
}
private void attachLibraries(@NotNull Collection<VirtualFile> libraryRoots, Set<VirtualFile> exclusions) {
ApplicationManager.getApplication().assertIsDispatchThread();
if (!libraryRoots.isEmpty()) {
ApplicationManager.getApplication().runWriteAction(() -> {
ModuleRootManager model = ModuleRootManager.getInstance(myModule);
LibraryOrderEntry goLibraryEntry = OrderEntryUtil.findLibraryOrderEntry(model, getLibraryName());
if (goLibraryEntry != null && goLibraryEntry.isValid()) {
Library library = goLibraryEntry.getLibrary();
if (library != null && !((LibraryEx)library).isDisposed()) {
fillLibrary(library, libraryRoots, exclusions);
}
}
else {
LibraryTable libraryTable = LibraryTablesRegistrar.getInstance().getLibraryTable(myModule.getProject());
Library library = libraryTable.createLibrary(getLibraryName());
fillLibrary(library, libraryRoots, exclusions);
ModuleRootModificationUtil.addDependency(myModule, library);
}
});
showNotification(myModule.getProject());
}
else {
removeLibraryIfNeeded();
}
}
public String getLibraryName() {
return GO_LIB_NAME + " <" + myModule.getName() + ">";
}
private static void fillLibrary(@NotNull Library library, @NotNull Collection<VirtualFile> libraryRoots, Set<VirtualFile> exclusions) {
ApplicationManager.getApplication().assertWriteAccessAllowed();
Library.ModifiableModel libraryModel = library.getModifiableModel();
for (String root : libraryModel.getUrls(OrderRootType.CLASSES)) {
libraryModel.removeRoot(root, OrderRootType.CLASSES);
}
for (String root : libraryModel.getUrls(OrderRootType.SOURCES)) {
libraryModel.removeRoot(root, OrderRootType.SOURCES);
}
for (VirtualFile libraryRoot : libraryRoots) {
libraryModel.addRoot(libraryRoot, OrderRootType.CLASSES); // in order to consider GOPATH as library and show it in Ext. Libraries
libraryModel.addRoot(libraryRoot, OrderRootType.SOURCES); // in order to find usages inside GOPATH
}
for (VirtualFile root : exclusions) {
((LibraryEx.ModifiableModelEx)libraryModel).addExcludedRoot(root.getUrl());
}
libraryModel.commit();
}
private void removeLibraryIfNeeded() {
ApplicationManager.getApplication().assertIsDispatchThread();
ModifiableModelsProvider modelsProvider = ModifiableModelsProvider.SERVICE.getInstance();
ModifiableRootModel model = modelsProvider.getModuleModifiableModel(myModule);
LibraryOrderEntry goLibraryEntry = OrderEntryUtil.findLibraryOrderEntry(model, getLibraryName());
if (goLibraryEntry != null) {
ApplicationManager.getApplication().runWriteAction(() -> {
Library library = goLibraryEntry.getLibrary();
if (library != null) {
LibraryTable table = library.getTable();
if (table != null) {
table.removeLibrary(library);
model.removeOrderEntry(goLibraryEntry);
modelsProvider.commitModuleModifiableModel(model);
}
}
else {
modelsProvider.disposeModuleModifiableModel(model);
}
});
}
else {
ApplicationManager.getApplication().runWriteAction(() -> modelsProvider.disposeModuleModifiableModel(model));
}
}
private static void showNotification(@NotNull Project project) {
PropertiesComponent propertiesComponent = PropertiesComponent.getInstance();
PropertiesComponent projectPropertiesComponent = PropertiesComponent.getInstance(project);
boolean shownAlready;
//noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized (propertiesComponent) {
shownAlready = propertiesComponent.getBoolean(GO_LIBRARIES_NOTIFICATION_HAD_BEEN_SHOWN, false)
|| projectPropertiesComponent.getBoolean(GO_LIBRARIES_NOTIFICATION_HAD_BEEN_SHOWN, false);
if (!shownAlready) {
propertiesComponent.setValue(GO_LIBRARIES_NOTIFICATION_HAD_BEEN_SHOWN, String.valueOf(true));
}
}
if (!shownAlready) {
NotificationListener.Adapter notificationListener = new NotificationListener.Adapter() {
@Override
protected void hyperlinkActivated(@NotNull Notification notification, @NotNull HyperlinkEvent event) {
if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED && "configure".equals(event.getDescription())) {
GoLibrariesConfigurableProvider.showModulesConfigurable(project);
}
}
};
Notification notification = GoConstants.GO_NOTIFICATION_GROUP.createNotification("GOPATH was detected",
"We've detected some libraries from your GOPATH.\n" +
"You may want to add extra libraries in <a href='configure'>Go Libraries configuration</a>.",
NotificationType.INFORMATION, notificationListener);
Notifications.Bus.notify(notification, project);
}
}
private void showVendoringNotification() {
if (!myModuleInitialized || myModule.isDisposed()) {
return;
}
Project project = myModule.getProject();
String version = GoSdkService.getInstance(project).getSdkVersion(myModule);
if (!GoVendoringUtil.supportsVendoring(version) || GoVendoringUtil.supportsVendoringByDefault(version)) {
return;
}
if (GoModuleSettings.getInstance(myModule).getVendoringEnabled() != ThreeState.UNSURE) {
return;
}
PropertiesComponent propertiesComponent = PropertiesComponent.getInstance(project);
boolean shownAlready;
//noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized (propertiesComponent) {
shownAlready = propertiesComponent.getBoolean(GO_VENDORING_NOTIFICATION_HAD_BEEN_SHOWN, false);
if (!shownAlready) {
propertiesComponent.setValue(GO_VENDORING_NOTIFICATION_HAD_BEEN_SHOWN, String.valueOf(true));
}
}
if (!shownAlready) {
NotificationListener.Adapter notificationListener = new NotificationListener.Adapter() {
@Override
protected void hyperlinkActivated(@NotNull Notification notification, @NotNull HyperlinkEvent event) {
if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED && "configure".equals(event.getDescription())) {
GoModuleSettings.showModulesConfigurable(project);
}
}
};
Notification notification = GoConstants.GO_NOTIFICATION_GROUP.createNotification("Vendoring usage is detected",
"<p><strong>vendor</strong> directory usually means that project uses Go Vendor Experiment.</p>\n" +
"<p>Selected Go SDK version support vendoring but it's disabled by default.</p>\n" +
"<p>You may want to explicitly enabled Go Vendor Experiment in the <a href='configure'>project settings</a>.</p>",
NotificationType.INFORMATION, notificationListener);
Notifications.Bus.notify(notification, project);
}
}
@Override
public void initComponent() {
}
@Override
public void disposeComponent() {
Disposer.dispose(myConnection);
Disposer.dispose(myAlarm);
VirtualFileManager.getInstance().removeVirtualFileListener(myFilesListener);
myLastHandledGoPathSourcesRoots.clear();
myLastHandledExclusions.clear();
LocalFileSystem.getInstance().removeWatchedRoots(myWatchedRequests);
myWatchedRequests.clear();
}
@Override
public void projectOpened() {
}
@Override
public void projectClosed() {
disposeComponent();
}
@NotNull
@Override
public String getComponentName() {
return getClass().getName();
}
private class UpdateRequest implements Runnable {
@Override
public void run() {
Project project = myModule.getProject();
if (GoSdkService.getInstance(project).isGoModule(myModule)) {
synchronized (myLastHandledGoPathSourcesRoots) {
Collection<VirtualFile> goPathSourcesRoots = GoSdkUtil.getGoPathSources(project, myModule);
Set<VirtualFile> excludeRoots = ContainerUtil.newHashSet(ProjectRootManager.getInstance(project).getContentRoots());
ProgressIndicatorProvider.checkCanceled();
if (!myLastHandledGoPathSourcesRoots.equals(goPathSourcesRoots) || !myLastHandledExclusions.equals(excludeRoots)) {
Collection<VirtualFile> includeRoots = gatherIncludeRoots(goPathSourcesRoots, excludeRoots);
ApplicationManager.getApplication().invokeLater(() -> {
if (!myModule.isDisposed() && GoSdkService.getInstance(project).isGoModule(myModule)) {
attachLibraries(includeRoots, excludeRoots);
}
});
myLastHandledGoPathSourcesRoots.clear();
myLastHandledGoPathSourcesRoots.addAll(goPathSourcesRoots);
myLastHandledExclusions.clear();
myLastHandledExclusions.addAll(excludeRoots);
List<String> paths = ContainerUtil.map(goPathSourcesRoots, VirtualFile::getPath);
myWatchedRequests.clear();
myWatchedRequests.addAll(LocalFileSystem.getInstance().addRootsToWatch(paths, true));
}
}
}
else {
synchronized (myLastHandledGoPathSourcesRoots) {
LocalFileSystem.getInstance().removeWatchedRoots(myWatchedRequests);
myLastHandledGoPathSourcesRoots.clear();
myLastHandledExclusions.clear();
ApplicationManager.getApplication().invokeLater(() -> {
if (!myModule.isDisposed() && GoSdkService.getInstance(project).isGoModule(myModule)) {
removeLibraryIfNeeded();
}
});
}
}
}
}
@NotNull
private static Collection<VirtualFile> gatherIncludeRoots(Collection<VirtualFile> goPathSourcesRoots, Set<VirtualFile> excludeRoots) {
Collection<VirtualFile> includeRoots = ContainerUtil.newHashSet();
for (VirtualFile goPathSourcesDirectory : goPathSourcesRoots) {
ProgressIndicatorProvider.checkCanceled();
boolean excludedRootIsAncestor = false;
for (VirtualFile excludeRoot : excludeRoots) {
ProgressIndicatorProvider.checkCanceled();
if (VfsUtilCore.isAncestor(excludeRoot, goPathSourcesDirectory, false)) {
excludedRootIsAncestor = true;
break;
}
}
if (excludedRootIsAncestor) {
continue;
}
for (VirtualFile file : goPathSourcesDirectory.getChildren()) {
ProgressIndicatorProvider.checkCanceled();
if (file.isDirectory() && !excludeRoots.contains(file)) {
includeRoots.add(file);
}
}
}
return includeRoots;
}
}