IDEA-108938 External system: Provide ability to detach linked external project
[idea/community.git] / platform / external-system-api / src / com / intellij / openapi / externalSystem / util / ExternalSystemApiUtil.java
1 /*
2  * Copyright 2000-2013 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.ide.util.PropertiesComponent;
19 import com.intellij.openapi.application.ApplicationManager;
20 import com.intellij.openapi.application.PathManager;
21 import com.intellij.openapi.diagnostic.Logger;
22 import com.intellij.openapi.externalSystem.ExternalSystemAutoImportAware;
23 import com.intellij.openapi.externalSystem.ExternalSystemManager;
24 import com.intellij.openapi.externalSystem.model.DataNode;
25 import com.intellij.openapi.externalSystem.model.Key;
26 import com.intellij.openapi.externalSystem.model.ProjectSystemId;
27 import com.intellij.openapi.fileTypes.FileTypes;
28 import com.intellij.openapi.project.Project;
29 import com.intellij.openapi.project.ProjectManager;
30 import com.intellij.openapi.roots.OrderRootType;
31 import com.intellij.openapi.roots.libraries.Library;
32 import com.intellij.openapi.util.AtomicNotNullLazyValue;
33 import com.intellij.openapi.util.NotNullLazyValue;
34 import com.intellij.openapi.util.io.FileUtil;
35 import com.intellij.openapi.util.text.StringUtil;
36 import com.intellij.openapi.vfs.JarFileSystem;
37 import com.intellij.openapi.vfs.VirtualFile;
38 import com.intellij.util.BooleanFunction;
39 import com.intellij.util.Function;
40 import com.intellij.util.PathUtil;
41 import com.intellij.util.PathsList;
42 import com.intellij.util.containers.ContainerUtilRt;
43 import com.intellij.util.ui.UIUtil;
44 import org.jetbrains.annotations.NotNull;
45 import org.jetbrains.annotations.Nullable;
46
47 import java.io.File;
48 import java.util.*;
49 import java.util.regex.Matcher;
50 import java.util.regex.Pattern;
51
52 /**
53  * @author Denis Zhdanov
54  * @since 4/1/13 1:31 PM
55  */
56 public class ExternalSystemApiUtil {
57
58   private static final Logger LOG                           =
59     Logger.getInstance("#" + ExternalSystemApiUtil.class.getName());
60   private static final String LAST_USED_PROJECT_PATH_PREFIX = "LAST_EXTERNAL_PROJECT_PATH_";
61
62   @NotNull public static final String PATH_SEPARATOR = "/";
63
64   @NotNull private static final Pattern ARTIFACT_PATTERN = Pattern.compile("(?:.*/)?(.+?)(?:-([\\d+](?:\\.[\\d]+)*))?(?:\\.[^\\.]+?)?");
65
66   @NotNull private static final NotNullLazyValue<Map<ProjectSystemId, ExternalSystemManager<?, ?, ?, ?, ?>>> MANAGERS =
67     new AtomicNotNullLazyValue<Map<ProjectSystemId, ExternalSystemManager<?, ?, ?, ?, ?>>>() {
68       @NotNull
69       @Override
70       protected Map<ProjectSystemId, ExternalSystemManager<?, ?, ?, ?, ?>> compute() {
71         Map<ProjectSystemId, ExternalSystemManager<?, ?, ?, ?, ?>> result = ContainerUtilRt.newHashMap();
72         for (ExternalSystemManager manager : ExternalSystemManager.EP_NAME.getExtensions()) {
73           result.put(manager.getSystemId(), manager);
74         }
75         return result;
76       }
77     };
78
79   @NotNull public static final Comparator<Object> ORDER_AWARE_COMPARATOR = new Comparator<Object>() {
80
81     @Override
82     public int compare(Object o1, Object o2) {
83       int order1 = getOrder(o1);
84       int order2 = getOrder(o2);
85       return order1 > order2 ? 1 : order1 < order2 ? -1 : 0;
86     }
87
88     private int getOrder(@NotNull Object o) {
89       Queue<Class<?>> toCheck = new ArrayDeque<Class<?>>();
90       toCheck.add(o.getClass());
91       while (!toCheck.isEmpty()) {
92         Class<?> clazz = toCheck.poll();
93         Order annotation = clazz.getAnnotation(Order.class);
94         if (annotation != null) {
95           return annotation.value();
96         }
97         toCheck.add(clazz.getSuperclass());
98         Class<?>[] interfaces = clazz.getInterfaces();
99         if (interfaces != null) {
100           Collections.addAll(toCheck, interfaces);
101         }
102       }
103       return ExternalSystemConstants.UNORDERED;
104     }
105   };
106
107   @NotNull private static final Function<DataNode<?>, Key<?>> GROUPER = new Function<DataNode<?>, Key<?>>() {
108     @Override
109     public Key<?> fun(DataNode<?> node) {
110       return node.getKey();
111     }
112   };
113
114   @NotNull private static final Comparator<Object> COMPARABLE_GLUE = new Comparator<Object>() {
115     @SuppressWarnings("unchecked")
116     @Override
117     public int compare(Object o1, Object o2) {
118       return ((Comparable)o1).compareTo(o2);
119     }
120   };
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   @Nullable
170   public static ArtifactInfo parseArtifactInfo(@NotNull String fileName) {
171     Matcher matcher = ARTIFACT_PATTERN.matcher(fileName);
172     if (!matcher.matches()) {
173       return null;
174     }
175     return new ArtifactInfo(matcher.group(1), null, matcher.group(2));
176   }
177
178   public static void orderAwareSort(@NotNull List<?> data) {
179     Collections.sort(data, ORDER_AWARE_COMPARATOR);
180   }
181
182   /**
183    * @param path    target path
184    * @return absolute path that points to the same location as the given one and that uses only slashes
185    */
186   @NotNull
187   public static String toCanonicalPath(@NotNull String path) {
188     return PathUtil.getCanonicalPath(new File(path).getAbsolutePath());
189   }
190
191   @NotNull
192   public static String getLocalFileSystemPath(@NotNull VirtualFile file) {
193     if (file.getFileType() == FileTypes.ARCHIVE) {
194       final VirtualFile jar = JarFileSystem.getInstance().getVirtualFileForJar(file);
195       if (jar != null) {
196         return jar.getPath();
197       }
198     }
199     return file.getPath();
200   }
201
202   @Nullable
203   public static ExternalSystemManager<?, ?, ?, ?, ?> getManager(@NotNull ProjectSystemId externalSystemId) {
204     return MANAGERS.getValue().get(externalSystemId);
205   }
206
207   public static Collection<ExternalSystemManager<?, ?, ?, ?, ?>> getAllManagers() {
208     return MANAGERS.getValue().values();
209   }
210
211   @NotNull
212   public static Map<Key<?>, List<DataNode<?>>> group(@NotNull Collection<DataNode<?>> nodes) {
213     return groupBy(nodes, GROUPER);
214   }
215
216   @NotNull
217   public static <K, V> Map<DataNode<K>, List<DataNode<V>>> groupBy(@NotNull Collection<DataNode<V>> nodes, @NotNull final Key<K> key) {
218     return groupBy(nodes, new Function<DataNode<V>, DataNode<K>>() {
219       @Override
220       public DataNode<K> fun(DataNode<V> node) {
221         return node.getDataNode(key);
222       }
223     });
224   }
225
226   @NotNull
227   public static <K, V> Map<K, List<V>> groupBy(@NotNull Collection<V> nodes, @NotNull Function<V, K> grouper) {
228     Map<K, List<V>> result = ContainerUtilRt.newHashMap();
229     for (V data : nodes) {
230       K key = grouper.fun(data);
231       if (key == null) {
232         LOG.warn(String.format(
233           "Skipping entry '%s' during grouping. Reason: it's not possible to build a grouping key with grouping strategy '%s'. "
234           + "Given entries: %s",
235           data,
236           grouper.getClass(),
237           nodes));
238         continue;
239       }
240       List<V> grouped = result.get(key);
241       if (grouped == null) {
242         result.put(key, grouped = ContainerUtilRt.newArrayList());
243       }
244       grouped.add(data);
245     }
246
247     if (!result.isEmpty() && result.keySet().iterator().next() instanceof Comparable) {
248       List<K> ordered = ContainerUtilRt.newArrayList(result.keySet());
249       Collections.sort(ordered, COMPARABLE_GLUE);
250       Map<K, List<V>> orderedResult = ContainerUtilRt.newLinkedHashMap();
251       for (K k : ordered) {
252         orderedResult.put(k, result.get(k));
253       }
254       return orderedResult;
255     }
256     return result;
257   }
258   
259   @SuppressWarnings("unchecked")
260   @NotNull
261   public static <T> Collection<DataNode<T>> getChildren(@NotNull DataNode<?> node, @NotNull Key<T> key) {
262     Collection<DataNode<T>> result = null;
263     for (DataNode<?> child : node.getChildren()) {
264       if (!key.equals(child.getKey())) {
265         continue;
266       }
267       if (result == null) {
268         result = ContainerUtilRt.newArrayList();
269       }
270       result.add((DataNode<T>)child);
271     }
272     return result == null ? Collections.<DataNode<T>>emptyList() : result;
273   }
274
275   @SuppressWarnings("unchecked")
276   @Nullable
277   public static <T> DataNode<T> find(@NotNull DataNode<?> node, @NotNull Key<T> key) {
278     for (DataNode<?> child : node.getChildren()) {
279       if (key.equals(child.getKey())) {
280         return (DataNode<T>)child;
281       }
282     }
283     return null;
284   }
285
286   @SuppressWarnings("unchecked")
287   @Nullable
288   public static <T> DataNode<T> find(@NotNull DataNode<?> node, @NotNull Key<T> key, BooleanFunction<DataNode<T>> predicate) {
289     for (DataNode<?> child : node.getChildren()) {
290       if (key.equals(child.getKey()) && predicate.fun((DataNode<T>)child)) {
291         return (DataNode<T>)child;
292       }
293     }
294     return null;
295   }
296
297   @SuppressWarnings("unchecked")
298   @NotNull
299   public static <T> Collection<DataNode<T>> findAll(@NotNull DataNode<?> parent, @NotNull Key<T> key) {
300     Collection<DataNode<T>> result = null;
301     for (DataNode<?> child : parent.getChildren()) {
302       if (!key.equals(child.getKey())) {
303         continue;
304       }
305       if (result == null) {
306         result = ContainerUtilRt.newArrayList();
307       }
308       result.add((DataNode<T>)child);
309     }
310     return result == null ? Collections.<DataNode<T>>emptyList() : result;
311   }
312
313   public static void executeProjectChangeAction(@NotNull final Runnable task) {
314     executeProjectChangeAction(false, task);
315   }
316
317   public static void executeProjectChangeAction(boolean synchronous, @NotNull final Runnable task) {
318     executeOnEdt(synchronous, new Runnable() {
319       public void run() {
320         ApplicationManager.getApplication().runWriteAction(new Runnable() {
321           @Override
322           public void run() {
323             task.run();
324           }
325         });
326       }
327     });
328   }
329   
330   public static void executeOnEdt(boolean synchronous, @NotNull Runnable task) {
331     if (synchronous) {
332       if (ApplicationManager.getApplication().isDispatchThread()) {
333         task.run();
334       }
335       else {
336         UIUtil.invokeAndWaitIfNeeded(task);
337       }
338     }
339     else {
340       UIUtil.invokeLaterIfNeeded(task);
341     }
342   }
343
344   /**
345    * Configures given classpath to reference target i18n bundle file(s).
346    *
347    * @param classPath     process classpath
348    * @param bundlePath    path to the target bundle file
349    * @param contextClass  class from the same content root as the target bundle file
350    */
351   public static void addBundle(@NotNull PathsList classPath, @NotNull String bundlePath, @NotNull Class<?> contextClass) {
352     String pathToUse = bundlePath.replace('.', '/');
353     if (!pathToUse.endsWith(".properties")) {
354       pathToUse += ".properties";
355     }
356     if (!pathToUse.startsWith("/")) {
357       pathToUse = '/' + pathToUse;
358     }
359     classPath.add(PathManager.getResourceRoot(contextClass, pathToUse));
360   }
361
362   @SuppressWarnings("ConstantConditions")
363   public static String normalizePath(String s) {
364     return StringUtil.isEmpty(s) ? null : s.replace('\\', ExternalSystemConstants.PATH_SEPARATOR);
365   }
366
367   /**
368    * We can divide all 'import from external system' use-cases into at least as below:
369    * <pre>
370    * <ul>
371    *   <li>this is a new project being created (import project from external model);</li>
372    *   <li>a new module is being imported from an external project into an existing ide project;</li>
373    * </ul>
374    * </pre>
375    * This method allows to differentiate between them (e.g. we don't want to change language level when new module is imported to
376    * an existing project).
377    * 
378    * @return    <code>true</code> if new project is being imported; <code>false</code> if new module is being imported
379    */
380   public static boolean isNewProjectConstruction() {
381     return ProjectManager.getInstance().getOpenProjects().length == 0;
382   }
383
384   @NotNull
385   public static String getLastUsedExternalProjectPath(@NotNull ProjectSystemId externalSystemId) {
386     return PropertiesComponent.getInstance().getValue(LAST_USED_PROJECT_PATH_PREFIX + externalSystemId.getReadableName(), "");
387   }
388
389   public static void storeLastUsedExternalProjectPath(@Nullable String path, @NotNull ProjectSystemId externalSystemId) {
390     if (path != null) {
391       PropertiesComponent.getInstance().setValue(LAST_USED_PROJECT_PATH_PREFIX + externalSystemId.getReadableName(), path);
392     }
393   }
394
395   @NotNull
396   public static String getProjectRepresentationName(@NotNull String targetProjectPath, @Nullable String rootProjectPath) {
397     if (rootProjectPath == null) {
398       return new File(targetProjectPath).getParentFile().getName();
399     }
400     File rootProjectDir = new File(rootProjectPath).getParentFile();
401     StringBuilder buffer = new StringBuilder();
402     for (File f = new File(targetProjectPath).getParentFile(); f != null && !FileUtil.filesEqual(f, rootProjectDir); f = f.getParentFile()) {
403       buffer.insert(0, f.getName()).insert(0, ":");
404     }
405     buffer.insert(0, rootProjectDir.getName());
406     return buffer.toString();
407   }
408
409   /**
410    * There is a possible case that external project linked to an ide project is a multi-project, i.e. contains more than one
411    * module.
412    * <p/>
413    * This method tries to find root project's config path assuming that given path points to a sub-project's config path.
414    * 
415    * @param externalProjectPath  external sub-project's config path
416    * @param externalSystemId     target external system
417    * @param project              target ide project
418    * @return                     root external project's path if given path is considered to point to a known sub-project's config;
419    *                             <code>null</code> if it's not possible to find a root project's config path on the basis of the
420    *                             given path
421    */
422   @Nullable
423   public static String getRootProjectPath(@NotNull String externalProjectPath,
424                                           @NotNull ProjectSystemId externalSystemId,
425                                           @NotNull Project project)
426   {
427     ExternalSystemManager<?, ?, ?, ?, ?> manager = getManager(externalSystemId);
428     if (manager == null) {
429       return null;
430     }
431     if (manager instanceof ExternalSystemAutoImportAware) {
432       return ((ExternalSystemAutoImportAware)manager).getAffectedExternalProjectPath(externalProjectPath, project);
433     }
434     return null;
435   }
436 }