replaced <code></code> with more concise {@code}
[idea/community.git] / plugins / gradle / src / org / jetbrains / plugins / gradle / remote / impl / GradleLibraryNamesMixer.java
1 package org.jetbrains.plugins.gradle.remote.impl;
2
3 import com.intellij.openapi.externalSystem.model.DataNode;
4 import com.intellij.openapi.externalSystem.model.project.LibraryData;
5 import com.intellij.openapi.externalSystem.model.project.LibraryPathType;
6 import com.intellij.openapi.util.text.StringUtil;
7 import com.intellij.util.containers.ContainerUtilRt;
8 import com.intellij.util.containers.HashMap;
9 import org.jetbrains.annotations.NotNull;
10 import org.jetbrains.annotations.Nullable;
11
12 import java.io.File;
13 import java.util.*;
14
15 /**
16  * Encapsulates logic of checking if particular collection of gradle libraries contains libraries with the same names and
17  * tries to diversify them in the case of the positive answer.
18  * <p/>
19  * Thread-safe.
20  * 
21  * @author Denis Zhdanov
22  * @since 10/19/11 2:04 PM
23  */
24 public class GradleLibraryNamesMixer {
25
26   /**
27    * Holds mappings like {@code ('file name'; boolean)} where {@code 'file name'} defines 'too common' file/dir
28    * name that should not be used during library name generation. Boolean flag indicates if 'common file name' may be used
29    * if 'non-common' files are the same.
30    * <p/>
31    * Example: consider the following file system tree:
32    * <pre>
33    *   module
34    *     |_src
35    *        |_main
36    *        |  |_resources
37    *        |
38    *        |_test
39    *           |_resources
40    * </pre>
41    * Let's say we have two libraries where one of them points to {@code 'src/main/resources'} and another one
42    * to {@code 'src/test/resources'}. We want to generate names {@code 'module-resources'} and
43    * {@code 'module-test-resources'} respectively because {@code 'test'} entry at the current collection is
44    * stored with {@code 'true'} flag.
45    */
46   private static final Map<String, Boolean> NON_UNIQUE_PATH_ENTRIES = new HashMap<>();
47   static {
48     NON_UNIQUE_PATH_ENTRIES.put("src", false);
49     NON_UNIQUE_PATH_ENTRIES.put("main", false);
50     NON_UNIQUE_PATH_ENTRIES.put("test", true);
51     NON_UNIQUE_PATH_ENTRIES.put("resources", false);
52     NON_UNIQUE_PATH_ENTRIES.put("java", false);
53     NON_UNIQUE_PATH_ENTRIES.put("groovy", false);
54   }
55   private static final char NAME_SEPARATOR = '-';
56
57   /**
58    * Tries to ensure that given libraries have distinct names, i.e. traverses all of them and tries to generate
59    * unique name for those with equal names.
60    * 
61    * @param libraries  libraries to process
62    */
63   @SuppressWarnings("MethodMayBeStatic")
64   public void mixNames(@NotNull Collection<DataNode<LibraryData>> libraries) {
65     if (libraries.isEmpty()) {
66       return;
67     }
68     Map<String, Wrapped> names = ContainerUtilRt.newHashMap();
69     List<Wrapped> data = ContainerUtilRt.newArrayList();
70     for (DataNode<LibraryData> library : libraries) {
71       Wrapped wrapped = new Wrapped(library.getData());
72       data.add(wrapped);
73     }
74     boolean mixed = false;
75     while (!mixed) {
76       mixed = doMixNames(data, names);
77     }
78   }
79
80   /**
81    * Does the same as {@link #mixNames(Collection)} but uses given {@code ('library name; wrapped library'}} mappings cache.
82    * 
83    * @param libraries  libraries to process
84    * @param cache      cache to use
85    * @return           {@code true} if all of the given libraries have distinct names now; {@code false} otherwise
86    */
87   private static boolean doMixNames(@NotNull Collection<Wrapped> libraries, @NotNull Map<String, Wrapped> cache) {
88     cache.clear();
89     for (Wrapped current : libraries) {
90       Wrapped previous = cache.remove(current.library.getExternalName());
91       if (previous == null) {
92         cache.put(current.library.getExternalName(), current);
93       }
94       else {
95         mixNames(current, previous);
96         return current.library.getExternalName().equals(previous.library.getExternalName()); // Stop processing if it's not possible to generate
97       }
98     }
99     return true;
100   }
101
102   /**
103    * Tries to generate distinct names for the given wrapped libraries (assuming that they have equal names at the moment).
104    * 
105    * @param wrapped1  one of the libraries with equal names
106    * @param wrapped2  another library which name is equal to the name of the given one
107    */
108   @SuppressWarnings("AssignmentToForLoopParameter")
109   private static void mixNames(@NotNull Wrapped wrapped1, @NotNull Wrapped wrapped2) {
110     if (!wrapped1.prepare() || !wrapped2.prepare()) {
111       return;
112     }
113     String wrapped1AltText = null;
114     String wrapped2AltText = null;
115     
116     for (File file1 = wrapped1.currentFile, file2 = wrapped2.currentFile;
117          file1 != null && file2 != null;
118          file1 = file1.getParentFile(), file2 = file2.getParentFile())
119     {
120       while (file1 != null && !StringUtil.isEmpty(file1.getName()) && NON_UNIQUE_PATH_ENTRIES.containsKey(file1.getName())) {
121         if (NON_UNIQUE_PATH_ENTRIES.get(file1.getName())) {
122           if (StringUtil.isEmpty(wrapped1AltText)) {
123             wrapped1AltText = file1.getName();
124           }
125           else {
126             wrapped1AltText += NAME_SEPARATOR + file1.getName();
127           }
128         }
129         file1 = file1.getParentFile();
130       }
131       while (file2 != null && !StringUtil.isEmpty(file2.getName()) && NON_UNIQUE_PATH_ENTRIES.containsKey(file2.getName())) {
132         if (NON_UNIQUE_PATH_ENTRIES.get(file2.getName())) {
133           if (StringUtil.isEmpty(wrapped2AltText)) {
134             wrapped2AltText = file2.getName();
135           }
136           else {
137             wrapped2AltText += NAME_SEPARATOR + file2.getName();
138           }
139         }
140         file2 = file2.getParentFile();
141       }
142       
143       if (file1 == null) {
144         wrapped1.nextFile();
145       }
146       else if (!wrapped1.library.getExternalName().startsWith(file1.getName())) {
147         wrapped1.library.setExternalName(file1.getName() + NAME_SEPARATOR + wrapped1.library.getExternalName());
148       }
149       if (file2 == null) {
150         wrapped2.nextFile();
151       }
152       else if (!wrapped2.library.getExternalName().startsWith(file2.getName())) {
153         wrapped2.library.setExternalName(file2.getName() + NAME_SEPARATOR + wrapped2.library.getExternalName());
154       }
155
156       if (wrapped1.library.getExternalName().equals(wrapped2.library.getExternalName())) {
157         if (wrapped1AltText != null) {
158           diversifyName(wrapped1AltText, wrapped1, file1);
159           return;
160         }
161         else if (wrapped2AltText != null) {
162           diversifyName(wrapped2AltText, wrapped2, file1);
163           return;
164         }
165       }
166       else {
167         return;
168       }
169
170       if (file1 == null || file2 == null) {
171         return;
172       }
173     }
174   }
175
176   @SuppressWarnings("ConstantConditions")
177   private static void diversifyName(@NotNull String changeText, @NotNull Wrapped wrapped, @Nullable File file) {
178     String name = wrapped.library.getExternalName();
179     int i = file == null ? - 1 : name.indexOf(file.getName());
180     final String newName;
181     if (i >= 0) {
182       newName = name.substring(0, i + file.getName().length()) + NAME_SEPARATOR + changeText + name.substring(i + file.getName().length());
183     }
184     else {
185       newName = changeText + NAME_SEPARATOR + name;
186     }
187     wrapped.library.setExternalName(newName);
188   }
189
190   /**
191    * Wraps target library and hold auxiliary information required for the processing.
192    */
193   private static class Wrapped {
194     /** Holds list of files that may be used for name generation. */
195     public final Set<File> files = new HashSet<>();
196     /** File that was used for the current name generation. */
197     public File        currentFile;
198     /** Target library. */
199     public LibraryData library;
200
201     Wrapped(@NotNull LibraryData library) {
202       this.library = library;
203       for (LibraryPathType pathType : LibraryPathType.values()) {
204         for (String path : library.getPaths(pathType)) {
205           files.add(new File(path));
206         }
207       }
208     }
209
210     public boolean prepare() {
211       if (currentFile != null) {
212         return true;
213       }
214       return nextFile();
215     }
216
217     public boolean nextFile() {
218       if (files.isEmpty()) {
219         return false;
220       }
221       Iterator<File> iterator = files.iterator();
222       currentFile = iterator.next();
223       iterator.remove();
224       return true;
225     }
226   }
227 }