2761502beffbcaae67c8cc1760ec4952e30b976c
[idea/community.git] / platform / external-system-api / src / com / intellij / openapi / externalSystem / util / ExternalSystemApiUtil.java
1 /*
2  * Copyright 2000-2017 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 com.intellij.openapi.externalSystem.util;
17
18 import com.intellij.execution.rmi.RemoteUtil;
19 import com.intellij.ide.util.PropertiesComponent;
20 import com.intellij.openapi.application.*;
21 import com.intellij.openapi.externalSystem.ExternalSystemAutoImportAware;
22 import com.intellij.openapi.externalSystem.ExternalSystemManager;
23 import com.intellij.openapi.externalSystem.ExternalSystemModulePropertyManager;
24 import com.intellij.openapi.externalSystem.model.DataNode;
25 import com.intellij.openapi.externalSystem.model.ExternalSystemException;
26 import com.intellij.openapi.externalSystem.model.Key;
27 import com.intellij.openapi.externalSystem.model.ProjectSystemId;
28 import com.intellij.openapi.externalSystem.model.project.LibraryData;
29 import com.intellij.openapi.externalSystem.model.project.ProjectData;
30 import com.intellij.openapi.externalSystem.model.settings.ExternalSystemExecutionSettings;
31 import com.intellij.openapi.externalSystem.service.ParametersEnhancer;
32 import com.intellij.openapi.externalSystem.settings.AbstractExternalSystemLocalSettings;
33 import com.intellij.openapi.externalSystem.settings.AbstractExternalSystemSettings;
34 import com.intellij.openapi.externalSystem.settings.ExternalProjectSettings;
35 import com.intellij.openapi.externalSystem.settings.ExternalSystemSettingsListener;
36 import com.intellij.openapi.fileTypes.FileTypes;
37 import com.intellij.openapi.module.Module;
38 import com.intellij.openapi.module.ModuleManager;
39 import com.intellij.openapi.project.Project;
40 import com.intellij.openapi.roots.ExternalProjectSystemRegistry;
41 import com.intellij.openapi.roots.ModifiableRootModel;
42 import com.intellij.openapi.roots.OrderRootType;
43 import com.intellij.openapi.roots.ProjectModelExternalSource;
44 import com.intellij.openapi.roots.libraries.Library;
45 import com.intellij.openapi.util.Computable;
46 import com.intellij.openapi.util.Conditions;
47 import com.intellij.openapi.util.Pair;
48 import com.intellij.openapi.util.Ref;
49 import com.intellij.openapi.util.io.FileUtil;
50 import com.intellij.openapi.util.registry.Registry;
51 import com.intellij.openapi.util.text.StringUtil;
52 import com.intellij.openapi.vfs.JarFileSystem;
53 import com.intellij.openapi.vfs.VirtualFile;
54 import com.intellij.util.*;
55 import com.intellij.util.containers.*;
56 import com.intellij.util.containers.Stack;
57 import com.intellij.util.lang.UrlClassLoader;
58 import com.intellij.util.ui.UIUtil;
59 import org.jetbrains.annotations.Contract;
60 import org.jetbrains.annotations.NotNull;
61 import org.jetbrains.annotations.Nullable;
62
63 import java.io.File;
64 import java.io.PrintWriter;
65 import java.io.StringWriter;
66 import java.lang.reflect.InvocationTargetException;
67 import java.lang.reflect.Method;
68 import java.net.URL;
69 import java.util.*;
70 import java.util.Queue;
71 import java.util.regex.Matcher;
72 import java.util.regex.Pattern;
73
74 /**
75  * @author Denis Zhdanov
76  * @since 4/1/13 1:31 PM
77  */
78 public class ExternalSystemApiUtil {
79   private static final String LAST_USED_PROJECT_PATH_PREFIX = "LAST_EXTERNAL_PROJECT_PATH_";
80
81   @NotNull public static final String PATH_SEPARATOR = "/";
82
83   @NotNull private static final Pattern ARTIFACT_PATTERN = Pattern.compile("(?:.*/)?(.+?)(?:-([\\d+](?:\\.[\\d]+)*))?(?:\\.[^\\.]+?)?");
84
85   @NotNull public static final Comparator<Object> ORDER_AWARE_COMPARATOR = new Comparator<Object>() {
86
87     @Override
88     public int compare(@NotNull Object o1, @NotNull Object o2) {
89       int order1 = getOrder(o1);
90       int order2 = getOrder(o2);
91       return (order1 < order2) ? -1 : ((order1 == order2) ? 0 : 1);
92     }
93
94     private int getOrder(@NotNull Object o) {
95       Queue<Class<?>> toCheck = new ArrayDeque<>();
96       toCheck.add(o.getClass());
97       while (!toCheck.isEmpty()) {
98         Class<?> clazz = toCheck.poll();
99         Order annotation = clazz.getAnnotation(Order.class);
100         if (annotation != null) {
101           return annotation.value();
102         }
103         Class<?> c = clazz.getSuperclass();
104         if (c != null) {
105           toCheck.add(c);
106         }
107         Class<?>[] interfaces = clazz.getInterfaces();
108         Collections.addAll(toCheck, interfaces);
109       }
110       return ExternalSystemConstants.UNORDERED;
111     }
112   };
113
114   @NotNull private static final NullableFunction<DataNode<?>, Key<?>> GROUPER = node -> node.getKey();
115
116   @NotNull private static final TransferToEDTQueue<Runnable> TRANSFER_TO_EDT_QUEUE =
117     new TransferToEDTQueue<>("External System queue", runnable -> {
118       runnable.run();
119       return true;
120     }, Conditions.alwaysFalse(), 300);
121
122   private ExternalSystemApiUtil() {
123   }
124
125   @NotNull
126   public static String extractNameFromPath(@NotNull String path) {
127     String strippedPath = stripPath(path);
128     final int i = strippedPath.lastIndexOf(PATH_SEPARATOR);
129     final String result;
130     if (i < 0 || i >= strippedPath.length() - 1) {
131       result = strippedPath;
132     }
133     else {
134       result = strippedPath.substring(i + 1);
135     }
136     return result;
137   }
138
139   @NotNull
140   private static String stripPath(@NotNull String path) {
141     String[] endingsToStrip = {"/", "!", ".jar"};
142     StringBuilder buffer = new StringBuilder(path);
143     for (String ending : endingsToStrip) {
144       if (buffer.lastIndexOf(ending) == buffer.length() - ending.length()) {
145         buffer.setLength(buffer.length() - ending.length());
146       }
147     }
148     return buffer.toString();
149   }
150
151   @NotNull
152   public static String getLibraryName(@NotNull Library library) {
153     final String result = library.getName();
154     if (result != null) {
155       return result;
156     }
157     for (OrderRootType type : OrderRootType.getAllTypes()) {
158       for (String url : library.getUrls(type)) {
159         String candidate = extractNameFromPath(url);
160         if (!StringUtil.isEmpty(candidate)) {
161           return candidate;
162         }
163       }
164     }
165     assert false;
166     return "unknown-lib";
167   }
168
169   public static boolean isRelated(@NotNull Library library, @NotNull LibraryData libraryData) {
170     return getLibraryName(library).equals(libraryData.getInternalName());
171   }
172
173   public static boolean isExternalSystemLibrary(@NotNull Library library, @NotNull ProjectSystemId externalSystemId) {
174     return library.getName() != null && StringUtil.startsWith(library.getName(), externalSystemId.getReadableName() + ": ");
175   }
176
177   @Nullable
178   public static ArtifactInfo parseArtifactInfo(@NotNull String fileName) {
179     Matcher matcher = ARTIFACT_PATTERN.matcher(fileName);
180     if (!matcher.matches()) {
181       return null;
182     }
183     return new ArtifactInfo(matcher.group(1), null, matcher.group(2));
184   }
185
186   public static void orderAwareSort(@NotNull List<?> data) {
187     Collections.sort(data, ORDER_AWARE_COMPARATOR);
188   }
189
190   /**
191    * @param path    target path
192    * @return absolute path that points to the same location as the given one and that uses only slashes
193    */
194   @NotNull
195   public static String toCanonicalPath(@NotNull String path) {
196     String p = normalizePath(new File(path).getAbsolutePath());
197     assert p != null;
198     return PathUtil.getCanonicalPath(p);
199   }
200
201   @NotNull
202   public static String getLocalFileSystemPath(@NotNull VirtualFile file) {
203     if (file.getFileType() == FileTypes.ARCHIVE) {
204       final VirtualFile jar = JarFileSystem.getInstance().getVirtualFileForJar(file);
205       if (jar != null) {
206         return jar.getPath();
207       }
208     }
209     return toCanonicalPath(file.getPath());
210   }
211
212   @Nullable
213   public static ExternalSystemManager<?, ?, ?, ?, ?> getManager(@NotNull ProjectSystemId externalSystemId) {
214     for (ExternalSystemManager manager : ExternalSystemManager.EP_NAME.getExtensions()) {
215       if (externalSystemId.equals(manager.getSystemId())) {
216         return manager;
217       }
218     }
219     return null;
220   }
221
222   @SuppressWarnings("ManualArrayToCollectionCopy")
223   @NotNull
224   public static Collection<ExternalSystemManager<?, ?, ?, ?, ?>> getAllManagers() {
225     List<ExternalSystemManager<?, ?, ?, ?, ?>> result = ContainerUtilRt.newArrayList();
226     for (ExternalSystemManager manager : ExternalSystemManager.EP_NAME.getExtensions()) {
227       result.add(manager);
228     }
229     return result;
230   }
231
232   public static MultiMap<Key<?>, DataNode<?>> recursiveGroup(@NotNull Collection<DataNode<?>> nodes) {
233     MultiMap<Key<?>, DataNode<?>> result = new ContainerUtil.KeyOrderedMultiMap<>();
234     Queue<Collection<DataNode<?>>> queue = ContainerUtil.newLinkedList();
235     queue.add(nodes);
236     while (!queue.isEmpty()) {
237       Collection<DataNode<?>> _nodes = queue.remove();
238       result.putAllValues(group(_nodes));
239       for (DataNode<?> _node : _nodes) {
240         queue.add(_node.getChildren());
241       }
242     }
243     return result;
244   }
245
246   @NotNull
247   public static MultiMap<Key<?>, DataNode<?>> group(@NotNull Collection<DataNode<?>> nodes) {
248     return ContainerUtil.groupBy(nodes, GROUPER);
249   }
250
251   @NotNull
252   public static <K, V> MultiMap<DataNode<K>, DataNode<V>> groupBy(@NotNull Collection<DataNode<V>> nodes, final Class<K> moduleDataClass) {
253     return ContainerUtil.groupBy(nodes, node -> node.getParent(moduleDataClass));
254   }
255
256   @NotNull
257   public static <K, V> MultiMap<DataNode<K>, DataNode<V>> groupBy(@NotNull Collection<DataNode<V>> nodes, @NotNull final Key<K> key) {
258     return ContainerUtil.groupBy(nodes, node -> node.getDataNode(key));
259   }
260
261   @SuppressWarnings("unchecked")
262   @NotNull
263   public static <T> Collection<DataNode<T>> getChildren(@NotNull DataNode<?> node, @NotNull Key<T> key) {
264     Collection<DataNode<T>> result = null;
265     for (DataNode<?> child : node.getChildren()) {
266       if (!key.equals(child.getKey())) {
267         continue;
268       }
269       if (result == null) {
270         result = ContainerUtilRt.newArrayList();
271       }
272       result.add((DataNode<T>)child);
273     }
274     return result == null ? Collections.emptyList() : result;
275   }
276
277   @SuppressWarnings("unchecked")
278   @Nullable
279   public static <T> DataNode<T> find(@NotNull DataNode<?> node, @NotNull Key<T> key) {
280     for (DataNode<?> child : node.getChildren()) {
281       if (key.equals(child.getKey())) {
282         return (DataNode<T>)child;
283       }
284     }
285     return null;
286   }
287
288   @SuppressWarnings("unchecked")
289   @Nullable
290   public static <T> DataNode<T> find(@NotNull DataNode<?> node, @NotNull Key<T> key, BooleanFunction<DataNode<T>> predicate) {
291     for (DataNode<?> child : node.getChildren()) {
292       if (key.equals(child.getKey()) && predicate.fun((DataNode<T>)child)) {
293         return (DataNode<T>)child;
294       }
295     }
296     return null;
297   }
298
299   @SuppressWarnings("unchecked")
300   @Nullable
301   public static <T> DataNode<T> findParent(@NotNull DataNode<?> node, @NotNull Key<T> key) {
302     return findParent(node, key, null);
303   }
304
305
306   @SuppressWarnings("unchecked")
307   @Nullable
308   public static <T> DataNode<T> findParent(@NotNull DataNode<?> node,
309                                            @NotNull Key<T> key,
310                                            @Nullable BooleanFunction<DataNode<T>> predicate) {
311     DataNode<?> parent = node.getParent();
312     if (parent == null) return null;
313     return key.equals(parent.getKey()) && (predicate == null || predicate.fun((DataNode<T>)parent))
314            ? (DataNode<T>)parent : findParent(parent, key, predicate);
315   }
316
317   @SuppressWarnings("unchecked")
318   @NotNull
319   public static <T> Collection<DataNode<T>> findAll(@NotNull DataNode<?> parent, @NotNull Key<T> key) {
320     return getChildren(parent, key);
321   }
322
323   public static void visit(@Nullable DataNode node, @NotNull Consumer<DataNode<?>> consumer) {
324     if(node == null) return;
325
326     Stack<DataNode> toProcess = ContainerUtil.newStack(node);
327     while (!toProcess.isEmpty()) {
328       DataNode<?> node0 = toProcess.pop();
329       consumer.consume(node0);
330       for (DataNode<?> child : node0.getChildren()) {
331         toProcess.push(child);
332       }
333     }
334   }
335
336   @NotNull
337   public static <T> Collection<DataNode<T>> findAllRecursively(@Nullable final DataNode<?> node,
338                                                                @NotNull final Key<T> key) {
339     if (node == null) return Collections.emptyList();
340
341     final Collection<DataNode<?>> nodes = findAllRecursively(node.getChildren(), node1 -> node1.getKey().equals(key));
342     //noinspection unchecked
343     return new SmartList(nodes);
344   }
345
346   @NotNull
347   public static Collection<DataNode<?>> findAllRecursively(@NotNull Collection<DataNode<?>> nodes) {
348     return findAllRecursively(nodes, null);
349   }
350
351   @NotNull
352   public static Collection<DataNode<?>> findAllRecursively(@Nullable DataNode<?> node,
353                                                            @Nullable BooleanFunction<DataNode<?>> predicate) {
354     if (node == null) return Collections.emptyList();
355     return findAllRecursively(node.getChildren(), predicate);
356   }
357
358   @NotNull
359   public static Collection<DataNode<?>> findAllRecursively(@NotNull Collection<DataNode<?>> nodes,
360                                                            @Nullable BooleanFunction<DataNode<?>> predicate) {
361     SmartList<DataNode<?>> result = new SmartList<>();
362     for (DataNode<?> node : nodes) {
363       if (predicate == null || predicate.fun(node)) {
364         result.add(node);
365       }
366     }
367     for (DataNode<?> node : nodes) {
368       result.addAll(findAllRecursively(node.getChildren(), predicate));
369     }
370     return result;
371   }
372
373   @Nullable
374   public static DataNode<?> findFirstRecursively(@NotNull DataNode<?> parentNode,
375                                                  @NotNull BooleanFunction<DataNode<?>> predicate) {
376     Queue<DataNode<?>> queue = new LinkedList<>();
377     queue.add(parentNode);
378     return findInQueue(queue, predicate);
379   }
380
381   @Nullable
382   public static DataNode<?> findFirstRecursively(@NotNull Collection<DataNode<?>> nodes,
383                                                  @NotNull BooleanFunction<DataNode<?>> predicate) {
384     return findInQueue(new LinkedList<>(nodes), predicate);
385   }
386
387   @Nullable
388   private static DataNode<?> findInQueue(@NotNull Queue<DataNode<?>> queue,
389                                          @NotNull BooleanFunction<DataNode<?>> predicate) {
390     while (!queue.isEmpty()) {
391       DataNode node = (DataNode)queue.remove();
392       if (predicate.fun(node)) {
393         return node;
394       }
395       //noinspection unchecked
396       queue.addAll(node.getChildren());
397     }
398     return null;
399   }
400
401   public static void commitChangedModels(boolean synchronous, Project project, List<Library.ModifiableModel> models) {
402     final List<Library.ModifiableModel> changedModels = ContainerUtil.findAll(models, model -> model.isChanged());
403     if (!changedModels.isEmpty()) {
404       executeProjectChangeAction(synchronous, new DisposeAwareProjectChange(project) {
405         @Override
406         public void execute() {
407           for (Library.ModifiableModel modifiableModel : changedModels) {
408             modifiableModel.commit();
409           }
410         }
411       });
412     }
413   }
414
415   public static void disposeModels(@NotNull Collection<ModifiableRootModel> models) {
416     for (ModifiableRootModel model : models) {
417       if (!model.isDisposed()) {
418         model.dispose();
419       }
420     }
421   }
422
423   public static void commitModels(boolean synchronous, Project project, List<ModifiableRootModel> models) {
424     final List<ModifiableRootModel> changedModels = ContainerUtilRt.newArrayList();
425     for (ModifiableRootModel modifiableRootModel : models) {
426       if (modifiableRootModel.isDisposed()) {
427         continue;
428       }
429       if (modifiableRootModel.isChanged()) {
430         changedModels.add(modifiableRootModel);
431       } else {
432         modifiableRootModel.dispose();
433       }
434     }
435     // Commit only if there are changes. #executeProjectChangeAction acquires a write lock
436     if (!changedModels.isEmpty()) {
437       executeProjectChangeAction(synchronous, new DisposeAwareProjectChange(project) {
438         @Override
439         public void execute() {
440           for (ModifiableRootModel modifiableRootModel : changedModels) {
441             // double check
442             if (!modifiableRootModel.isDisposed()) {
443               modifiableRootModel.commit();
444             }
445           }
446         }
447       });
448     }
449   }
450
451   public static void executeProjectChangeAction(@NotNull final DisposeAwareProjectChange task) {
452     executeProjectChangeAction(true, task);
453   }
454
455   public static void executeProjectChangeAction(boolean synchronous, @NotNull final DisposeAwareProjectChange task) {
456     TransactionGuard.getInstance().assertWriteSafeContext(ModalityState.defaultModalityState());
457     executeOnEdt(synchronous, () -> ApplicationManager.getApplication().runWriteAction(() -> task.run()));
458   }
459
460   public static void executeOnEdt(boolean synchronous, @NotNull Runnable task) {
461     final Application app = ApplicationManager.getApplication();
462     if (app.isDispatchThread()) {
463       task.run();
464       return;
465     }
466     
467     if (synchronous) {
468       app.invokeAndWait(task);
469     }
470     else {
471       app.invokeLater(task);
472     }
473   }
474
475   public static <T> T executeOnEdt(@NotNull final Computable<T> task) {
476     final Application app = ApplicationManager.getApplication();
477     final Ref<T> result = Ref.create();
478     app.invokeAndWait(() -> result.set(task.compute()));
479     return result.get();
480   }
481
482   public static <T> T doWriteAction(@NotNull final Computable<T> task) {
483     return executeOnEdt(() -> ApplicationManager.getApplication().runWriteAction(task));
484   }
485
486   public static void doWriteAction(@NotNull final Runnable task) {
487     executeOnEdt(true, () -> ApplicationManager.getApplication().runWriteAction(task));
488   }
489
490   /**
491   * Adds runnable to Event Dispatch Queue
492   * if we aren't in UnitTest of Headless environment mode
493   *
494   * @param runnable Runnable
495   */
496   public static void addToInvokeLater(final Runnable runnable) {
497     final Application application = ApplicationManager.getApplication();
498     final boolean unitTestMode = application.isUnitTestMode();
499     if (unitTestMode) {
500       UIUtil.invokeLaterIfNeeded(runnable);
501     }
502     else if (application.isHeadlessEnvironment() || application.isDispatchThread()) {
503       runnable.run();
504     }
505     else {
506       TRANSFER_TO_EDT_QUEUE.offer(runnable);
507     }
508   }
509
510   /**
511    * Configures given classpath to reference target i18n bundle file(s).
512    *
513    * @param classPath     process classpath
514    * @param bundlePath    path to the target bundle file
515    * @param contextClass  class from the same content root as the target bundle file
516    */
517   public static void addBundle(@NotNull PathsList classPath, @NotNull String bundlePath, @NotNull Class<?> contextClass) {
518     String pathToUse = bundlePath.replace('.', '/');
519     if (!pathToUse.endsWith(".properties")) {
520       pathToUse += ".properties";
521     }
522     if (!pathToUse.startsWith("/")) {
523       pathToUse = '/' + pathToUse;
524     }
525     String root = PathManager.getResourceRoot(contextClass, pathToUse);
526     if (root != null) {
527       classPath.add(root);
528     }
529   }
530
531   @SuppressWarnings("ConstantConditions")
532   @Nullable
533   public static String normalizePath(@Nullable String s) {
534     return StringUtil.isEmpty(s) ? null : s.replace('\\', ExternalSystemConstants.PATH_SEPARATOR);
535   }
536
537   /**
538    * Allows to answer if given ide project has 1-1 mapping with the given external project, i.e. the ide project has been
539    * imported from external system and no other external projects have been added.
540    * <p/>
541    * This might be necessary in a situation when project-level setting is changed (e.g. project name). We don't want to rename
542    * ide project if it doesn't completely corresponds to the given ide project then.
543    *
544    * @param ideProject       target ide project
545    * @param projectData      target external project
546    * @return                 <code>true</code> if given ide project has 1-1 mapping to the given external project;
547    *                         <code>false</code> otherwise
548    */
549   public static boolean isOneToOneMapping(@NotNull Project ideProject, @NotNull ProjectData projectData) {
550     String linkedExternalProjectPath = null;
551     for (ExternalSystemManager<?, ?, ?, ?, ?> manager : getAllManagers()) {
552       ProjectSystemId externalSystemId = manager.getSystemId();
553       AbstractExternalSystemSettings systemSettings = getSettings(ideProject, externalSystemId);
554       Collection projectsSettings = systemSettings.getLinkedProjectsSettings();
555       int linkedProjectsNumber = projectsSettings.size();
556       if (linkedProjectsNumber > 1) {
557         // More than one external project of the same external system type is linked to the given ide project.
558         return false;
559       }
560       else if (linkedProjectsNumber == 1) {
561         if (linkedExternalProjectPath == null) {
562           // More than one external project of different external system types is linked to the current ide project.
563           linkedExternalProjectPath = ((ExternalProjectSettings)projectsSettings.iterator().next()).getExternalProjectPath();
564         }
565         else {
566           return false;
567         }
568       }
569     }
570
571     if (linkedExternalProjectPath != null && !linkedExternalProjectPath.equals(projectData.getLinkedExternalProjectPath())) {
572       // New external project is being linked.
573       return false;
574     }
575
576     for (Module module : ModuleManager.getInstance(ideProject).getModules()) {
577       if (!isExternalSystemAwareModule(projectData.getOwner(), module)) {
578         return false;
579       }
580     }
581     return true;
582   }
583
584   public static void storeLastUsedExternalProjectPath(@Nullable String path, @NotNull ProjectSystemId externalSystemId) {
585     if (path != null) {
586       PropertiesComponent.getInstance().setValue(LAST_USED_PROJECT_PATH_PREFIX + externalSystemId.getReadableName(), path);
587     }
588   }
589
590   @NotNull
591   public static String getProjectRepresentationName(@NotNull String targetProjectPath, @Nullable String rootProjectPath) {
592     if (rootProjectPath == null) {
593       File rootProjectDir = new File(targetProjectPath);
594       if (rootProjectDir.isFile()) {
595         rootProjectDir = rootProjectDir.getParentFile();
596       }
597       return rootProjectDir.getName();
598     }
599     File rootProjectDir = new File(rootProjectPath);
600     if (rootProjectDir.isFile()) {
601       rootProjectDir = rootProjectDir.getParentFile();
602     }
603     File targetProjectDir = new File(targetProjectPath);
604     if (targetProjectDir.isFile()) {
605       targetProjectDir = targetProjectDir.getParentFile();
606     }
607     StringBuilder buffer = new StringBuilder();
608     for (File f = targetProjectDir; f != null && !FileUtil.filesEqual(f, rootProjectDir); f = f.getParentFile()) {
609       buffer.insert(0, f.getName()).insert(0, ":");
610     }
611     buffer.insert(0, rootProjectDir.getName());
612     return buffer.toString();
613   }
614
615   /**
616    * There is a possible case that external project linked to an ide project is a multi-project, i.e. contains more than one
617    * module.
618    * <p/>
619    * This method tries to find root project's config path assuming that given path points to a sub-project's config path.
620    *
621    * @param externalProjectPath  external sub-project's config path
622    * @param externalSystemId     target external system
623    * @param project              target ide project
624    * @return                     root external project's path if given path is considered to point to a known sub-project's config;
625    *                             <code>null</code> if it's not possible to find a root project's config path on the basis of the
626    *                             given path
627    */
628   @Nullable
629   public static String getRootProjectPath(@NotNull String externalProjectPath,
630                                           @NotNull ProjectSystemId externalSystemId,
631                                           @NotNull Project project)
632   {
633     ExternalSystemManager<?, ?, ?, ?, ?> manager = getManager(externalSystemId);
634     if (manager == null) {
635       return null;
636     }
637     if (manager instanceof ExternalSystemAutoImportAware) {
638       return ((ExternalSystemAutoImportAware)manager).getAffectedExternalProjectPath(externalProjectPath, project);
639     }
640     return null;
641   }
642
643   /**
644    * {@link RemoteUtil#unwrap(Throwable) unwraps} given exception if possible and builds error message for it.
645    *
646    * @param e  exception to process
647    * @return error message for the given exception
648    */
649   @SuppressWarnings({"ThrowableResultOfMethodCallIgnored", "IOResourceOpenedButNotSafelyClosed"})
650   @NotNull
651   public static String buildErrorMessage(@NotNull Throwable e) {
652     Throwable unwrapped = RemoteUtil.unwrap(e);
653     if (ApplicationManager.getApplication().isUnitTestMode()) {
654       return stacktraceAsString(unwrapped);
655     }
656     String reason = unwrapped.getLocalizedMessage();
657     if (!StringUtil.isEmpty(reason)) {
658       return reason;
659     }
660     else if (unwrapped.getClass() == ExternalSystemException.class) {
661       return String.format("exception during working with external system: %s", ((ExternalSystemException)unwrapped).getOriginalReason());
662     }
663     else {
664       return stacktraceAsString(unwrapped);
665     }
666   }
667
668   private static String stacktraceAsString(Throwable unwrapped) {
669     StringWriter writer = new StringWriter();
670     unwrapped.printStackTrace(new PrintWriter(writer));
671     return writer.toString();
672   }
673
674   @SuppressWarnings("unchecked")
675   @NotNull
676   public static AbstractExternalSystemSettings getSettings(@NotNull Project project, @NotNull ProjectSystemId externalSystemId)
677     throws IllegalArgumentException
678   {
679     ExternalSystemManager<?, ?, ?, ?, ?> manager = getManager(externalSystemId);
680     if (manager == null) {
681       throw new IllegalArgumentException(String.format(
682         "Can't retrieve external system settings for id '%s'. Reason: no such external system is registered",
683         externalSystemId.getReadableName()
684       ));
685     }
686     return manager.getSettingsProvider().fun(project);
687   }
688
689   @SuppressWarnings("unchecked")
690   public static <S extends AbstractExternalSystemLocalSettings> S getLocalSettings(@NotNull Project project,
691                                                                                    @NotNull ProjectSystemId externalSystemId)
692     throws IllegalArgumentException
693   {
694     ExternalSystemManager<?, ?, ?, ?, ?> manager = getManager(externalSystemId);
695     if (manager == null) {
696       throw new IllegalArgumentException(String.format(
697         "Can't retrieve local external system settings for id '%s'. Reason: no such external system is registered",
698         externalSystemId.getReadableName()
699       ));
700     }
701     return (S)manager.getLocalSettingsProvider().fun(project);
702   }
703
704   @SuppressWarnings("unchecked")
705   public static <S extends ExternalSystemExecutionSettings> S getExecutionSettings(@NotNull Project project,
706                                                                             @NotNull String linkedProjectPath,
707                                                                             @NotNull ProjectSystemId externalSystemId)
708     throws IllegalArgumentException
709   {
710     ExternalSystemManager<?, ?, ?, ?, ?> manager = getManager(externalSystemId);
711     if (manager == null) {
712       throw new IllegalArgumentException(String.format(
713         "Can't retrieve external system execution settings for id '%s'. Reason: no such external system is registered",
714         externalSystemId.getReadableName()
715       ));
716     }
717     return (S)manager.getExecutionSettingsProvider().fun(Pair.create(project, linkedProjectPath));
718   }
719
720   /**
721    * Historically we prefer to work with third-party api not from ide process but from dedicated slave process (there is a risk
722    * that third-party api has bugs which might make the whole ide process corrupted, e.g. a memory leak at the api might crash
723    * the whole ide process).
724    * <p/>
725    * However, we do allow to explicitly configure the ide to work with third-party external system api from the ide process.
726    * <p/>
727    * This method allows to check whether the ide is configured to use 'out of process' or 'in process' mode for the system.
728    *
729    * @param externalSystemId     target external system
730    *
731    * @return   <code>true</code> if the ide is configured to work with external system api from the ide process;
732    *           <code>false</code> otherwise
733    */
734   public static boolean isInProcessMode(ProjectSystemId externalSystemId) {
735     return Registry.is(externalSystemId.getId() + ExternalSystemConstants.USE_IN_PROCESS_COMMUNICATION_REGISTRY_KEY_SUFFIX, false);
736   }
737
738   /**
739    * There is a possible case that methods of particular object should be executed with classpath different from the one implied
740    * by the current class' class loader. External system offers {@link ParametersEnhancer#enhanceLocalProcessing(List)} method
741    * for defining that custom classpath.
742    * <p/>
743    * It's also possible that particular implementation of {@link ParametersEnhancer} is compiled using dependency to classes
744    * which are provided by the {@link ParametersEnhancer#enhanceLocalProcessing(List) expanded classpath}. E.g. a class
745    * <code>'A'</code> might use method of class <code>'B'</code> and 'A' is located at the current (system/plugin) classpath but
746    * <code>'B'</code> is not. We need to reload <code>'A'</code> using its expanded classpath then, i.e. create new class loaded
747    * with that expanded classpath and load <code>'A'</code> by it.
748    * <p/>
749    * This method allows to do that.
750    *
751    * @param clazz  custom classpath-aware class which instance should be created (is assumed to have a no-args constructor)
752    * @param <T>    target type
753    * @return       newly created instance of the given class loaded by custom classpath-aware loader
754    * @throws IllegalAccessException     as defined by reflection processing
755    * @throws InstantiationException     as defined by reflection processing
756    * @throws NoSuchMethodException      as defined by reflection processing
757    * @throws InvocationTargetException  as defined by reflection processing
758    * @throws ClassNotFoundException     as defined by reflection processing
759    */
760   @NotNull
761   public static <T extends ParametersEnhancer> T reloadIfNecessary(@NotNull final Class<T> clazz)
762     throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, ClassNotFoundException
763   {
764     T instance = clazz.newInstance();
765     List<URL> urls = ContainerUtilRt.newArrayList();
766     instance.enhanceLocalProcessing(urls);
767     if (urls.isEmpty()) {
768       return instance;
769     }
770
771     final ClassLoader baseLoader = clazz.getClassLoader();
772     Method method = baseLoader.getClass().getMethod("getUrls");
773     if (method != null) {
774       //noinspection unchecked
775       urls.addAll((Collection<? extends URL>)method.invoke(baseLoader));
776     }
777     UrlClassLoader loader = new UrlClassLoader(UrlClassLoader.build().urls(urls).parent(baseLoader.getParent())) {
778       @Override
779       protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
780         if (name.equals(clazz.getName())) {
781           return super.loadClass(name, resolve);
782         }
783         else {
784           try {
785             return baseLoader.loadClass(name);
786           }
787           catch (ClassNotFoundException e) {
788             return super.loadClass(name, resolve);
789           }
790         }
791       }
792     };
793     //noinspection unchecked
794     return (T)loader.loadClass(clazz.getName()).newInstance();
795   }
796
797   public static ProjectModelExternalSource toExternalSource(@NotNull ProjectSystemId systemId) {
798     return ExternalProjectSystemRegistry.getInstance().getSourceById(systemId.getId());
799   }
800
801   @Contract(value = "_, null -> false", pure=true)
802   public static boolean isExternalSystemAwareModule(@NotNull ProjectSystemId systemId, @Nullable Module module) {
803     return module != null && !module.isDisposed() && systemId.getId().equals(ExternalSystemModulePropertyManager.getInstance(module).getExternalSystemId());
804   }
805
806   @Contract(value = "_, null -> false", pure=true)
807   public static boolean isExternalSystemAwareModule(@NotNull String systemId, @Nullable Module module) {
808     return module != null && !module.isDisposed() && systemId.equals(ExternalSystemModulePropertyManager.getInstance(module).getExternalSystemId());
809   }
810
811   @Nullable
812   @Contract(pure=true)
813   public static String getExternalProjectPath(@Nullable Module module) {
814     return module != null && !module.isDisposed() ? ExternalSystemModulePropertyManager.getInstance(module).getLinkedProjectPath() : null;
815   }
816
817   @Nullable
818   @Contract(pure=true)
819   public static String getExternalRootProjectPath(@Nullable Module module) {
820     return module != null && !module.isDisposed() ? ExternalSystemModulePropertyManager.getInstance(module).getRootProjectPath() : null;
821   }
822
823   @Nullable
824   @Contract(pure=true)
825   public static String getExternalProjectId(@Nullable Module module) {
826     return module != null && !module.isDisposed() ? ExternalSystemModulePropertyManager.getInstance(module).getLinkedProjectId() : null;
827   }
828
829   @Nullable
830   @Contract(pure=true)
831   public static String getExternalProjectGroup(@Nullable Module module) {
832     return module != null && !module.isDisposed() ? ExternalSystemModulePropertyManager.getInstance(module).getExternalModuleGroup() : null;
833   }
834
835   @Nullable
836   @Contract(pure=true)
837   public static String getExternalProjectVersion(@Nullable Module module) {
838     return module != null && !module.isDisposed() ? ExternalSystemModulePropertyManager.getInstance(module).getExternalModuleVersion() : null;
839   }
840
841   @Nullable
842   @Contract(pure=true)
843   public static String getExternalModuleType(@Nullable Module module) {
844     return module != null && !module.isDisposed() ? ExternalSystemModulePropertyManager.getInstance(module).getExternalModuleType() : null;
845   }
846
847   public static void subscribe(@NotNull Project project,
848                                @NotNull ProjectSystemId systemId,
849                                @NotNull ExternalSystemSettingsListener listener) {
850     //noinspection unchecked
851     getSettings(project, systemId).subscribe(listener);
852   }
853 }