4a0cb90817870dd246aa630f88e4bef7667c0480
[idea/community.git] / plugins / svn4idea / src / org / jetbrains / idea / svn / SvnFileUrlMappingImpl.java
1 /*
2  * Copyright 2000-2016 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package org.jetbrains.idea.svn;
17
18 import com.intellij.lifecycle.PeriodicalTasksCloser;
19 import com.intellij.openapi.application.ApplicationManager;
20 import com.intellij.openapi.components.*;
21 import com.intellij.openapi.diagnostic.Logger;
22 import com.intellij.openapi.progress.ProcessCanceledException;
23 import com.intellij.openapi.project.DumbAwareRunnable;
24 import com.intellij.openapi.project.Project;
25 import com.intellij.openapi.util.io.FileUtil;
26 import com.intellij.openapi.vcs.ProjectLevelVcsManager;
27 import com.intellij.openapi.vcs.ThreadLocalDefendedInvoker;
28 import com.intellij.openapi.vcs.impl.ProjectLevelVcsManagerImpl;
29 import com.intellij.openapi.vcs.impl.VcsInitObject;
30 import com.intellij.openapi.vfs.LocalFileSystem;
31 import com.intellij.openapi.vfs.VirtualFile;
32 import com.intellij.util.containers.ContainerUtil;
33 import com.intellij.util.messages.MessageBus;
34 import org.jetbrains.annotations.NotNull;
35 import org.jetbrains.annotations.Nullable;
36 import org.jetbrains.idea.svn.info.Info;
37 import org.tmatesoft.svn.core.SVNException;
38 import org.tmatesoft.svn.core.SVNURL;
39 import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
40
41 import java.io.File;
42 import java.util.List;
43 import java.util.Set;
44
45 @State(name = "SvnFileUrlMappingImpl", storages = @Storage(StoragePathMacros.WORKSPACE_FILE))
46 public class SvnFileUrlMappingImpl implements SvnFileUrlMapping, PersistentStateComponent<SvnMappingSavedPart>, ProjectComponent {
47   private static final Logger LOG = Logger.getInstance("#org.jetbrains.idea.svn.SvnFileUrlMappingImpl");
48
49   private final SvnCompatibilityChecker myChecker;
50   private final Object myMonitor = new Object();
51   // strictly: what real roots are under what vcs mappings
52   private final SvnMapping myMapping;
53   // grouped; if there are several mappings one under another, will return the upmost
54   private final SvnMapping myMoreRealMapping;
55   private final List<RootUrlInfo> myErrorRoots;
56   private final MyRootsHelper myHelper;
57   private final Project myProject;
58   private final NestedCopiesHolder myNestedCopiesHolder;
59   private boolean myInitialized;
60   private boolean myInitedReloaded;
61
62   private static class MyRootsHelper extends ThreadLocalDefendedInvoker<VirtualFile[]> {
63     private final ProjectLevelVcsManager myPlVcsManager;
64
65     private MyRootsHelper(final ProjectLevelVcsManager vcsManager) {
66       myPlVcsManager = vcsManager;
67     }
68
69     protected VirtualFile[] execute(Project project) {
70       return myPlVcsManager.getRootsUnderVcs(SvnVcs.getInstance(project));
71     }
72   }
73
74   public static SvnFileUrlMappingImpl getInstance(final Project project) {
75     return PeriodicalTasksCloser.getInstance().safeGetComponent(project, SvnFileUrlMappingImpl.class);
76   }
77
78   @SuppressWarnings("UnusedDeclaration")
79   private SvnFileUrlMappingImpl(final Project project, final ProjectLevelVcsManager vcsManager) {
80     myProject = project;
81     myMapping = new SvnMapping();
82     myMoreRealMapping = new SvnMapping();
83     myErrorRoots = ContainerUtil.newArrayList();
84     myHelper = new MyRootsHelper(vcsManager);
85     myChecker = new SvnCompatibilityChecker(project);
86     myNestedCopiesHolder = new NestedCopiesHolder();
87   }
88
89   @Nullable
90   public SVNURL getUrlForFile(final File file) {
91     final RootUrlInfo rootUrlInfo = getWcRootForFilePath(file);
92     if (rootUrlInfo == null) {
93       return null;
94     }
95
96     final String absolutePath = file.getAbsolutePath();
97     final String rootAbsPath = rootUrlInfo.getIoFile().getAbsolutePath();
98     if (absolutePath.length() < rootAbsPath.length()) {
99       // remove last separator from etalon name
100       if (absolutePath.equals(rootAbsPath.substring(0, rootAbsPath.length() - 1))) {
101         return rootUrlInfo.getAbsoluteUrlAsUrl();
102       }
103       return null;
104     }
105     final String relativePath = absolutePath.substring(rootAbsPath.length());
106     try {
107       return rootUrlInfo.getAbsoluteUrlAsUrl().appendPath(FileUtil.toSystemIndependentName(relativePath), true);
108     }
109     catch (SVNException e) {
110       LOG.info(e);
111       return null;
112     }
113   }
114
115   @Nullable
116   public File getLocalPath(@NotNull String url) {
117     synchronized (myMonitor) {
118       final String rootUrl = getUrlRootForUrl(url);
119       if (rootUrl == null) {
120         return null;
121       }
122       final RootUrlInfo parentInfo = myMoreRealMapping.byUrl(rootUrl);
123       if (parentInfo == null) {
124         return null;
125       }
126
127       return new File(parentInfo.getIoFile(), url.substring(rootUrl.length()));
128     }
129   }
130
131   @Nullable
132   public RootUrlInfo getWcRootForFilePath(final File file) {
133     synchronized (myMonitor) {
134       final String root = getRootForPath(file);
135       if (root == null) {
136         return null;
137       }
138
139       return myMoreRealMapping.byFile(root);
140     }
141   }
142
143   public boolean rootsDiffer() {
144     synchronized (myMonitor) {
145       return myMapping.isRootsDifferFromSettings();
146     }
147   }
148
149   @Nullable
150   public RootUrlInfo getWcRootForUrl(final String url) {
151     synchronized (myMonitor) {
152       final String rootUrl = getUrlRootForUrl(url);
153       if (rootUrl == null) {
154         return null;
155       }
156
157       final RootUrlInfo result = myMoreRealMapping.byUrl(rootUrl);
158       if (result == null) {
159         LOG.info("Inconsistent maps for url:" + url + " found root url: " + rootUrl);
160         return null;
161       }
162       return result;
163     }
164   }
165
166   /**
167    * Returns real working copies roots - if there is <Project Root> -> Subversion setting,
168    * and there is one working copy, will return one root
169    */
170   public List<RootUrlInfo> getAllWcInfos() {
171     synchronized (myMonitor) {
172       // a copy is created inside
173       return myMoreRealMapping.getAllCopies();
174     }
175   }
176
177   @Override
178   public List<RootUrlInfo> getErrorRoots() {
179     synchronized (myMonitor) {
180       return ContainerUtil.newArrayList(myErrorRoots);
181     }
182   }
183
184   @NotNull
185   public List<VirtualFile> convertRoots(@NotNull List<VirtualFile> result) {
186     if (ThreadLocalDefendedInvoker.isInside()) return ContainerUtil.newArrayList(result);
187
188     synchronized (myMonitor) {
189       final List<VirtualFile> cachedRoots = myMoreRealMapping.getUnderVcsRoots();
190       final List<VirtualFile> lonelyRoots = myMoreRealMapping.getLonelyRoots();
191       if (! lonelyRoots.isEmpty()) {
192         myChecker.reportNoRoots(lonelyRoots);
193       }
194
195       return ContainerUtil.newArrayList(cachedRoots.isEmpty() ? result : cachedRoots);
196     }
197   }
198
199   public void acceptNestedData(final Set<NestedCopyInfo> set) {
200     myNestedCopiesHolder.add(set);
201   }
202
203   private boolean init() {
204     synchronized (myMonitor) {
205       final boolean result = myInitialized;
206       myInitialized = true;
207       return result;
208     }
209   }
210
211   public void realRefresh(final Runnable afterRefreshCallback) {
212     if (myProject.isDisposed()) {
213       afterRefreshCallback.run();
214     }
215     else {
216       final SvnVcs vcs = SvnVcs.getInstance(myProject);
217       final VirtualFile[] roots = myHelper.executeDefended(myProject);
218       final SvnRootsDetector rootsDetector = new SvnRootsDetector(vcs, this, myNestedCopiesHolder);
219       // do not send additional request for nested copies when in init state
220       rootsDetector.detectCopyRoots(roots, init(), afterRefreshCallback);
221     }
222   }
223
224   public void applyDetectionResult(@NotNull SvnRootsDetector.Result result) {
225     new NewRootsApplier(result).apply();
226   }
227
228   private class NewRootsApplier {
229
230     @NotNull private final SvnRootsDetector.Result myResult;
231     @NotNull private final SvnMapping myNewMapping;
232     @NotNull private final SvnMapping myNewFilteredMapping;
233
234     private NewRootsApplier(@NotNull SvnRootsDetector.Result result) {
235       myResult = result;
236       myNewMapping = new SvnMapping();
237       myNewFilteredMapping = new SvnMapping();
238     }
239
240     public void apply() {
241       myNewMapping.addAll(myResult.getTopRoots());
242       myNewMapping.reportLonelyRoots(myResult.getLonelyRoots());
243       myNewFilteredMapping.addAll(new UniqueRootsFilter().filter(myResult.getTopRoots()));
244
245       runUpdateMappings();
246     }
247
248     private void runUpdateMappings() {
249       // TODO: Not clear so far why read action is used here - may be because of ROOTS_RELOADED message sent?
250       ApplicationManager.getApplication().runReadAction(new Runnable() {
251         @Override
252         public void run() {
253           if (myProject.isDisposed()) return;
254
255           boolean mappingsChanged = updateMappings();
256
257           notifyRootsReloaded(mappingsChanged);
258         }
259       });
260     }
261
262     private boolean updateMappings() {
263       boolean mappingsChanged;
264       synchronized (myMonitor) {
265         mappingsChanged = ! myMoreRealMapping.equals(myNewFilteredMapping);
266         mappingsChanged |= !myErrorRoots.equals(myResult.getErrorRoots());
267
268         myMapping.copyFrom(myNewMapping);
269         myMoreRealMapping.copyFrom(myNewFilteredMapping);
270         myErrorRoots.clear();
271         myErrorRoots.addAll(myResult.getErrorRoots());
272       }
273       return mappingsChanged;
274     }
275
276     private void notifyRootsReloaded(boolean mappingsChanged) {
277       final MessageBus bus = myProject.getMessageBus();
278       if (mappingsChanged || ! myInitedReloaded) {
279         myInitedReloaded = true;
280         // all listeners are asynchronous
281         bus.syncPublisher(SvnVcs.ROOTS_RELOADED).consume(true);
282         bus.syncPublisher(ProjectLevelVcsManager.VCS_CONFIGURATION_CHANGED_IN_PLUGIN).directoryMappingChanged();
283       } else {
284         bus.syncPublisher(SvnVcs.ROOTS_RELOADED).consume(false);
285       }
286     }
287   }
288
289   @Nullable
290   public String getUrlRootForUrl(final String currentUrl) {
291     for (String url : myMoreRealMapping.getUrls()) {
292       if (SVNPathUtil.isAncestor(url, currentUrl)) {
293         return url;
294       }
295     }
296     return null;
297   }
298
299   @Nullable
300   public String getRootForPath(final File currentPath) {
301     String convertedPath = currentPath.getAbsolutePath();
302     convertedPath = (currentPath.isDirectory() && (! convertedPath.endsWith(File.separator))) ? convertedPath + File.separator :
303         convertedPath;
304     synchronized (myMonitor) {
305       return myMoreRealMapping.getRootForPath(convertedPath);
306     }
307   }
308
309   public VirtualFile[] getNotFilteredRoots() {
310     return myHelper.executeDefended(myProject);
311   }
312
313   public boolean isEmpty() {
314     synchronized (myMonitor) {
315       return myMapping.isEmpty();
316     }
317   }
318
319   public SvnMappingSavedPart getState() {
320     final SvnMappingSavedPart result = new SvnMappingSavedPart();
321
322     final SvnMapping mapping = new SvnMapping();
323     final SvnMapping realMapping = new SvnMapping();
324     synchronized (myMonitor) {
325       mapping.copyFrom(myMapping);
326       realMapping.copyFrom(myMoreRealMapping);
327     }
328
329     for (RootUrlInfo info : mapping.getAllCopies()) {
330       result.add(convert(info));
331     }
332     for (RootUrlInfo info : realMapping.getAllCopies()) {
333       result.addReal(convert(info));
334     }
335     return result;
336   }
337
338   private SvnCopyRootSimple convert(final RootUrlInfo info) {
339     final SvnCopyRootSimple copy = new SvnCopyRootSimple();
340     copy.myVcsRoot = FileUtil.toSystemDependentName(info.getRoot().getPath());
341     copy.myCopyRoot = info.getIoFile().getAbsolutePath();
342     return copy;
343   }
344
345   public void loadState(final SvnMappingSavedPart state) {
346     ((ProjectLevelVcsManagerImpl) ProjectLevelVcsManager.getInstance(myProject)).addInitializationRequest(
347       VcsInitObject.AFTER_COMMON, new DumbAwareRunnable() {
348         public void run() {
349           ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
350             @Override
351             public void run() {
352               final SvnMapping mapping = new SvnMapping();
353               final SvnMapping realMapping = new SvnMapping();
354               try {
355                 fillMapping(mapping, state.getMappingRoots());
356                 fillMapping(realMapping, state.getMoreRealMappingRoots());
357               } catch (ProcessCanceledException e) {
358                 throw e;
359               } catch (Throwable t) {
360                 LOG.info(t);
361                 return;
362               }
363
364               synchronized (myMonitor) {
365                 myMapping.copyFrom(mapping);
366                 myMoreRealMapping.copyFrom(realMapping);
367               }
368             }
369           });
370         }
371     });
372   }
373
374   private void fillMapping(final SvnMapping mapping, final List<SvnCopyRootSimple> list) {
375     final LocalFileSystem lfs = LocalFileSystem.getInstance();
376
377     for (SvnCopyRootSimple simple : list) {
378       final VirtualFile copyRoot = lfs.findFileByIoFile(new File(simple.myCopyRoot));
379       final VirtualFile vcsRoot = lfs.findFileByIoFile(new File(simple.myVcsRoot));
380
381       if (copyRoot == null || vcsRoot == null) continue;
382
383       final SvnVcs vcs = SvnVcs.getInstance(myProject);
384       final Info svnInfo = vcs.getInfo(copyRoot);
385       if ((svnInfo == null) || (svnInfo.getRepositoryRootURL() == null)) continue;
386
387       Node node = new Node(copyRoot, svnInfo.getURL(), svnInfo.getRepositoryRootURL());
388       final RootUrlInfo info = new RootUrlInfo(node, SvnFormatSelector.findRootAndGetFormat(svnInfo.getFile()), vcsRoot);
389
390       mapping.add(info);
391     }
392   }
393
394   public void projectOpened() {
395   }
396
397   public void projectClosed() {
398   }
399
400   @NotNull
401   public String getComponentName() {
402     return "SvnFileUrlMappingImpl";
403   }
404
405   public void initComponent() {
406   }
407
408   public void disposeComponent() {
409   }
410 }