| /* |
| * Copyright 2000-2013 JetBrains s.r.o. |
| * |
| * 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.intellij.openapi.externalSystem.service.project.autoimport; |
| |
| import com.intellij.openapi.components.ServiceManager; |
| import com.intellij.openapi.editor.Document; |
| import com.intellij.openapi.editor.EditorFactory; |
| import com.intellij.openapi.editor.event.DocumentEvent; |
| import com.intellij.openapi.editor.event.DocumentListener; |
| import com.intellij.openapi.externalSystem.ExternalSystemAutoImportAware; |
| import com.intellij.openapi.externalSystem.ExternalSystemManager; |
| import com.intellij.openapi.externalSystem.model.DataNode; |
| import com.intellij.openapi.externalSystem.model.ProjectSystemId; |
| import com.intellij.openapi.externalSystem.model.project.ProjectData; |
| import com.intellij.openapi.externalSystem.service.project.ExternalProjectRefreshCallback; |
| import com.intellij.openapi.externalSystem.service.project.manage.ProjectDataManager; |
| import com.intellij.openapi.externalSystem.settings.AbstractExternalSystemSettings; |
| import com.intellij.openapi.externalSystem.settings.ExternalProjectSettings; |
| import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil; |
| import com.intellij.openapi.externalSystem.util.ExternalSystemConstants; |
| import com.intellij.openapi.externalSystem.util.ExternalSystemUtil; |
| import com.intellij.openapi.externalSystem.service.execution.ProgressExecutionMode; |
| import com.intellij.openapi.fileEditor.FileDocumentManager; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.roots.ex.ProjectRootManagerEx; |
| import com.intellij.openapi.vfs.LocalFileSystem; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.openapi.vfs.VirtualFileManager; |
| import com.intellij.openapi.vfs.newvfs.BulkFileListener; |
| import com.intellij.openapi.vfs.newvfs.events.VFileEvent; |
| import com.intellij.util.Alarm; |
| import com.intellij.util.containers.ContainerUtil; |
| import com.intellij.util.containers.ContainerUtilRt; |
| import com.intellij.util.messages.MessageBus; |
| import com.intellij.util.ui.UIUtil; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.util.*; |
| import java.util.concurrent.ConcurrentMap; |
| import java.util.concurrent.locks.Lock; |
| import java.util.concurrent.locks.ReadWriteLock; |
| import java.util.concurrent.locks.ReentrantReadWriteLock; |
| |
| /** |
| * @author Denis Zhdanov |
| * @since 6/7/13 6:38 PM |
| */ |
| public class ExternalSystemAutoImporter implements BulkFileListener, DocumentListener { |
| |
| @NotNull private final ConcurrentMap<ProjectSystemId, Set<String /* external project path */>> myFilesToRefresh |
| = ContainerUtil.newConcurrentMap(); |
| |
| @NotNull private final Alarm myVfsAlarm = new Alarm(Alarm.ThreadToUse.SHARED_THREAD); |
| @NotNull private final ReadWriteLock myVfsLock = new ReentrantReadWriteLock(); |
| |
| @NotNull private final Set<Document> myDocumentsToSave = ContainerUtilRt.newHashSet(); |
| @NotNull private final Alarm myDocumentAlarm = new Alarm(Alarm.ThreadToUse.SHARED_THREAD); |
| @NotNull private final ReadWriteLock myDocumentLock = new ReentrantReadWriteLock(); |
| |
| @NotNull private final Runnable myFilesRequest = new Runnable() { |
| @Override |
| public void run() { |
| refreshFilesIfNecessary(); |
| } |
| }; |
| @NotNull private final Runnable myDocumentsSaveRequest = new Runnable() { |
| @Override |
| public void run() { |
| saveDocumentsIfNecessary(); |
| } |
| }; |
| @NotNull private final ExternalProjectRefreshCallback myRefreshCallback = new ExternalProjectRefreshCallback() { |
| @Override |
| public void onSuccess(@Nullable final DataNode<ProjectData> externalProject) { |
| if (externalProject != null) { |
| ExternalSystemApiUtil.executeProjectChangeAction(new Runnable() { |
| @Override |
| public void run() { |
| ProjectRootManagerEx.getInstanceEx(myProject).mergeRootsChangesDuring(new Runnable() { |
| @Override |
| public void run() { |
| myProjectDataManager.importData(externalProject.getKey(), Collections.singleton(externalProject), myProject, true); |
| } |
| }); |
| } |
| }); |
| } |
| } |
| |
| @Override |
| public void onFailure(@NotNull String errorMessage, @Nullable String errorDetails) { |
| // Do nothing. |
| } |
| }; |
| |
| @NotNull private final Project myProject; |
| @NotNull private final ProjectDataManager myProjectDataManager; |
| |
| @NotNull private final MyEntry[] myAutoImportAware; |
| |
| public ExternalSystemAutoImporter(@NotNull Project project, |
| @NotNull ProjectDataManager projectDataManager, |
| @NotNull MyEntry[] autoImportAware) |
| { |
| myProject = project; |
| myProjectDataManager = projectDataManager; |
| myAutoImportAware = autoImportAware; |
| } |
| |
| @SuppressWarnings("unchecked") |
| public static void letTheMagicBegin(@NotNull Project project) { |
| List<MyEntry> autoImportAware = ContainerUtilRt.newArrayList(); |
| Collection<ExternalSystemManager<?, ?, ?, ?, ?>> managers = ExternalSystemApiUtil.getAllManagers(); |
| for (ExternalSystemManager<?, ?, ?, ?, ?> manager : managers) { |
| AbstractExternalSystemSettings<?, ?, ?> systemSettings = manager.getSettingsProvider().fun(project); |
| ExternalSystemAutoImportAware defaultImportAware = createDefault(systemSettings); |
| final ExternalSystemAutoImportAware aware; |
| if (manager instanceof ExternalSystemAutoImportAware) { |
| aware = combine(defaultImportAware, (ExternalSystemAutoImportAware)manager); |
| } |
| else { |
| aware = defaultImportAware; |
| } |
| autoImportAware.add(new MyEntry(manager.getSystemId(), systemSettings, aware)); |
| } |
| |
| MyEntry[] entries = autoImportAware.toArray(new MyEntry[autoImportAware.size()]); |
| ExternalSystemAutoImporter autoImporter = new ExternalSystemAutoImporter( |
| project, |
| ServiceManager.getService(ProjectDataManager.class), |
| entries |
| ); |
| final MessageBus messageBus = project.getMessageBus(); |
| messageBus.connect().subscribe(VirtualFileManager.VFS_CHANGES, autoImporter); |
| |
| EditorFactory.getInstance().getEventMulticaster().addDocumentListener(autoImporter, project); |
| } |
| |
| @NotNull |
| private static ExternalSystemAutoImportAware combine(@NotNull final ExternalSystemAutoImportAware aware1, |
| @NotNull final ExternalSystemAutoImportAware aware2) |
| { |
| return new ExternalSystemAutoImportAware() { |
| @Nullable |
| @Override |
| public String getAffectedExternalProjectPath(@NotNull String changedFileOrDirPath, @NotNull Project project) { |
| String projectPath = aware1.getAffectedExternalProjectPath(changedFileOrDirPath, project); |
| return projectPath == null ? aware2.getAffectedExternalProjectPath(changedFileOrDirPath, project) : projectPath; |
| } |
| }; |
| } |
| |
| @NotNull |
| private static ExternalSystemAutoImportAware createDefault(@NotNull final AbstractExternalSystemSettings<?, ?, ?> systemSettings) { |
| return new ExternalSystemAutoImportAware() { |
| @Nullable |
| @Override |
| public String getAffectedExternalProjectPath(@NotNull String changedFileOrDirPath, @NotNull Project project) { |
| return systemSettings.getLinkedProjectSettings(changedFileOrDirPath) == null ? null : changedFileOrDirPath; |
| } |
| }; |
| } |
| |
| @Override |
| public void beforeDocumentChange(DocumentEvent event) { |
| } |
| |
| @Override |
| public void documentChanged(DocumentEvent event) { |
| Document document = event.getDocument(); |
| FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance(); |
| VirtualFile file = fileDocumentManager.getFile(document); |
| if (file == null) { |
| return; |
| } |
| |
| String path = ExternalSystemApiUtil.getLocalFileSystemPath(file); |
| for (MyEntry entry : myAutoImportAware) { |
| if (entry.aware.getAffectedExternalProjectPath(path, myProject) != null) { |
| // Document save triggers VFS event but FileDocumentManager might be registered after the current listener, that's why |
| // call to 'saveDocument()' might not produce the desired effect. That's why we reschedule document save if necessary. |
| scheduleDocumentSave(document); |
| return; |
| } |
| } |
| } |
| |
| private void scheduleDocumentSave(@NotNull Document document) { |
| Lock lock = myDocumentLock.readLock(); |
| lock.lock(); |
| try { |
| myDocumentsToSave.add(document); |
| if (myDocumentAlarm.getActiveRequestCount() <= 0) { |
| myDocumentAlarm.addRequest(myDocumentsSaveRequest, 100); |
| } |
| } |
| finally { |
| lock.unlock(); |
| } |
| } |
| |
| private void saveDocumentsIfNecessary() { |
| final FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance(); |
| Lock lock = myDocumentLock.writeLock(); |
| Set<Document> toKeep = ContainerUtilRt.newHashSet(); |
| final Set<Document> toSave = ContainerUtilRt.newHashSet(); |
| lock.lock(); |
| try { |
| myDocumentAlarm.cancelAllRequests(); |
| for (Document document : myDocumentsToSave) { |
| if (fileDocumentManager.isDocumentUnsaved(document)) { |
| toSave.add(document); |
| } |
| else { |
| toKeep.add(document); |
| } |
| } |
| myDocumentsToSave.clear(); |
| if (!toSave.isEmpty()) { |
| UIUtil.invokeLaterIfNeeded(new Runnable() { |
| @Override |
| public void run() { |
| for (Document document : toSave) { |
| fileDocumentManager.saveDocument(document); |
| } |
| } |
| }); |
| } |
| if (!toKeep.isEmpty()) { |
| myDocumentsToSave.addAll(toKeep); |
| myDocumentAlarm.addRequest(myDocumentsSaveRequest, 100); |
| } |
| } |
| finally { |
| lock.unlock(); |
| } |
| } |
| |
| @Override |
| public void before(@NotNull List<? extends VFileEvent> events) { |
| } |
| |
| @Override |
| public void after(@NotNull List<? extends VFileEvent> events) { |
| boolean scheduleRefresh = false; |
| for (VFileEvent event : events) { |
| String changedPath = event.getPath(); |
| for (MyEntry entry : myAutoImportAware) { |
| String projectPath = entry.aware.getAffectedExternalProjectPath(changedPath, myProject); |
| if (projectPath == null) { |
| continue; |
| } |
| ExternalProjectSettings projectSettings = entry.systemSettings.getLinkedProjectSettings(projectPath); |
| if (projectSettings != null && projectSettings.isUseAutoImport()) { |
| addPath(entry.externalSystemId, projectPath); |
| scheduleRefresh = true; |
| break; |
| } |
| } |
| } |
| if (scheduleRefresh) { |
| myVfsAlarm.cancelAllRequests(); |
| myVfsAlarm.addRequest(myFilesRequest, ExternalSystemConstants.AUTO_IMPORT_DELAY_MILLIS); |
| } |
| } |
| |
| private void addPath(@NotNull ProjectSystemId externalSystemId, @NotNull String path) { |
| Lock lock = myVfsLock.readLock(); |
| lock.lock(); |
| try { |
| Set<String> paths = myFilesToRefresh.get(externalSystemId); |
| while (paths == null) { |
| myFilesToRefresh.putIfAbsent(externalSystemId, ContainerUtilRt.<String>newHashSet()); |
| paths = myFilesToRefresh.get(externalSystemId); |
| } |
| paths.add(path); |
| } |
| finally { |
| lock.unlock(); |
| } |
| } |
| |
| private void refreshFilesIfNecessary() { |
| if (myFilesToRefresh.isEmpty()) { |
| return; |
| } |
| |
| Map<ProjectSystemId, Set<String>> copy = ContainerUtilRt.newHashMap(); |
| Lock fileLock = myVfsLock.writeLock(); |
| fileLock.lock(); |
| try { |
| copy.putAll(myFilesToRefresh); |
| myFilesToRefresh.clear(); |
| } |
| finally { |
| fileLock.unlock(); |
| } |
| |
| FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance(); |
| LocalFileSystem fileSystem = LocalFileSystem.getInstance(); |
| Lock documentLock = myDocumentLock.writeLock(); |
| documentLock.lock(); |
| try { |
| for (Set<String> paths : copy.values()) { |
| for (String path : paths) { |
| VirtualFile file = fileSystem.findFileByPath(path); |
| if (file != null) { |
| Document document = fileDocumentManager.getCachedDocument(file); |
| if (document != null) { |
| myDocumentsToSave.remove(document); |
| } |
| } |
| } |
| } |
| } |
| finally { |
| documentLock.unlock(); |
| } |
| |
| for (Map.Entry<ProjectSystemId, Set<String>> entry : copy.entrySet()) { |
| for (String path : entry.getValue()) { |
| ExternalSystemUtil.refreshProject(myProject, entry.getKey(), path, myRefreshCallback, false, ProgressExecutionMode.IN_BACKGROUND_ASYNC, false); |
| } |
| } |
| } |
| |
| private static class MyEntry { |
| |
| @NotNull public final ProjectSystemId externalSystemId; |
| @NotNull public final AbstractExternalSystemSettings<?, ?, ?> systemSettings; |
| @NotNull public final ExternalSystemAutoImportAware aware; |
| |
| MyEntry(@NotNull ProjectSystemId externalSystemId, |
| @NotNull AbstractExternalSystemSettings<?, ?, ?> systemSettings, |
| @NotNull ExternalSystemAutoImportAware aware) |
| { |
| this.externalSystemId = externalSystemId; |
| this.systemSettings = systemSettings; |
| this.aware = aware; |
| } |
| } |
| } |