bc963277682a38acf351ee4f9a637109bfc5b276
[idea/community.git] / platform / util / src / com / intellij / openapi / application / PathManager.java
1 /*
2  * Copyright 2000-2015 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.application;
17
18 import com.intellij.openapi.util.*;
19 import com.intellij.openapi.util.io.FileUtil;
20 import com.intellij.openapi.util.text.StringUtil;
21 import com.intellij.util.io.URLUtil;
22 import com.sun.jna.TypeMapper;
23 import com.sun.jna.platform.FileUtils;
24 import gnu.trove.THashSet;
25 import org.apache.log4j.Appender;
26 import org.apache.oro.text.regex.PatternMatcher;
27 import org.jdom.Document;
28 import org.jetbrains.annotations.Contract;
29 import org.jetbrains.annotations.NotNull;
30 import org.jetbrains.annotations.Nullable;
31 import org.picocontainer.PicoContainer;
32
33 import java.io.*;
34 import java.net.URL;
35 import java.util.*;
36 import java.util.regex.Matcher;
37 import java.util.regex.Pattern;
38
39 import static com.intellij.util.SystemProperties.getUserHome;
40
41 public class PathManager {
42   public static final String PROPERTIES_FILE = "idea.properties.file";
43   public static final String PROPERTY_HOME_PATH = "idea.home.path";
44   public static final String PROPERTY_CONFIG_PATH = "idea.config.path";
45   public static final String PROPERTY_SYSTEM_PATH = "idea.system.path";
46   public static final String PROPERTY_PLUGINS_PATH = "idea.plugins.path";
47   public static final String PROPERTY_LOG_PATH = "idea.log.path";
48   public static final String PROPERTY_PATHS_SELECTOR = "idea.paths.selector";
49   public static final String DEFAULT_OPTIONS_FILE_NAME = "other";
50
51   private static final String PROPERTY_HOME = "idea.home";  // reduced variant of PROPERTY_HOME_PATH, now deprecated
52
53   private static final String LIB_FOLDER = "lib";
54   private static final String PLUGINS_FOLDER = "plugins";
55   private static final String BIN_FOLDER = "bin";
56   private static final String LOG_DIRECTORY = "log";
57   private static final String CONFIG_FOLDER = "config";
58   private static final String OPTIONS_FOLDER = "options";
59   private static final String SYSTEM_FOLDER = "system";
60   private static final String PATHS_SELECTOR = System.getProperty(PROPERTY_PATHS_SELECTOR);
61
62   private static final Pattern PROPERTY_REF = Pattern.compile("\\$\\{(.+?)}");
63
64   private static String ourHomePath;
65   private static String ourConfigPath;
66   private static String ourSystemPath;
67   private static String ourPluginsPath;
68   private static String ourLogPath;
69
70   // IDE installation paths
71
72   @NotNull
73   public static String getHomePath() {
74     if (ourHomePath != null) return ourHomePath;
75
76     String fromProperty = System.getProperty(PROPERTY_HOME_PATH, System.getProperty(PROPERTY_HOME));
77     if (fromProperty != null) {
78       ourHomePath = getAbsolutePath(fromProperty);
79       if (!new File(ourHomePath).isDirectory()) {
80         throw new RuntimeException("Invalid home path '" + ourHomePath + "'");
81       }
82     }
83     else {
84       ourHomePath = getHomePathFor(PathManager.class);
85       if (ourHomePath == null) {
86         String advice = SystemInfo.isMac ? "reinstall the software."
87                                          : "make sure bin/idea.properties is present in the installation directory.";
88         throw new RuntimeException("Could not find installation home path. Please " + advice);
89       }
90     }
91
92     if (SystemInfo.isWindows) {
93       try {
94         ourHomePath = new File(ourHomePath).getCanonicalPath();
95       }
96       catch (IOException ignored) { }
97     }
98
99     return ourHomePath;
100   }
101
102   @Nullable
103   public static String getHomePathFor(@NotNull Class aClass) {
104     String rootPath = getResourceRoot(aClass, "/" + aClass.getName().replace('.', '/') + ".class");
105     if (rootPath == null) return null;
106
107     File root = new File(rootPath).getAbsoluteFile();
108     do {
109       String parent = root.getParent();
110       if (parent == null) return null;
111       root = new File(parent).getAbsoluteFile();  // one step back to get folder
112     }
113     while (!isIdeaHome(root));
114     return root.getAbsolutePath();
115   }
116
117   private static boolean isIdeaHome(final File root) {
118     return new File(root, FileUtil.toSystemDependentName("bin/idea.properties")).exists() ||
119            new File(root, FileUtil.toSystemDependentName("community/bin/idea.properties")).exists();
120   }
121
122   @NotNull
123   public static String getBinPath() {
124     return getHomePath() + File.separator + BIN_FOLDER;
125   }
126
127   @NotNull
128   public static String getLibPath() {
129     return getHomePath() + File.separator + LIB_FOLDER;
130   }
131
132   @SuppressWarnings("MethodNamesDifferingOnlyByCase")
133   @NotNull
134   public static String getPreInstalledPluginsPath() {
135     return getHomePath() + File.separatorChar + PLUGINS_FOLDER;
136   }
137
138   // config paths
139
140   @Nullable
141   public static String getPathsSelector() {
142     return PATHS_SELECTOR;
143   }
144
145   @NotNull
146   public static String getConfigPath() {
147     if (ourConfigPath != null) return ourConfigPath;
148
149     if (System.getProperty(PROPERTY_CONFIG_PATH) != null) {
150       ourConfigPath = getAbsolutePath(trimPathQuotes(System.getProperty(PROPERTY_CONFIG_PATH)));
151     }
152     else if (PATHS_SELECTOR != null) {
153       ourConfigPath = getDefaultConfigPathFor(PATHS_SELECTOR);
154     }
155     else {
156       ourConfigPath = getHomePath() + File.separator + CONFIG_FOLDER;
157     }
158
159     return ourConfigPath;
160   }
161
162   @NotNull
163   public static String getDefaultConfigPathFor(@NotNull String selector) {
164     return platformPath(selector, "Library/Preferences", CONFIG_FOLDER);
165   }
166
167   public static void ensureConfigFolderExists() {
168     checkAndCreate(getConfigPath(), true);
169   }
170
171   @NotNull
172   public static String getOptionsPath() {
173     return getConfigPath() + File.separator + OPTIONS_FOLDER;
174   }
175
176   @NotNull
177   public static File getOptionsFile(@NotNull String fileName) {
178     return new File(getOptionsPath(), fileName + ".xml");
179   }
180
181   @NotNull
182   public static File getOptionsFile(@NotNull NamedJDOMExternalizable externalizable) {
183     return getOptionsFile(externalizable.getExternalFileName());
184   }
185
186   @NotNull
187   public static String getPluginsPath() {
188     if (ourPluginsPath != null) return ourPluginsPath;
189
190     if (System.getProperty(PROPERTY_PLUGINS_PATH) != null) {
191       ourPluginsPath = getAbsolutePath(trimPathQuotes(System.getProperty(PROPERTY_PLUGINS_PATH)));
192     }
193     else if (SystemInfo.isMac && PATHS_SELECTOR != null) {
194       ourPluginsPath = getUserHome() + File.separator + "Library/Application Support" + File.separator + PATHS_SELECTOR;
195     }
196     else {
197       ourPluginsPath = getConfigPath() + File.separatorChar + PLUGINS_FOLDER;
198     }
199
200     return ourPluginsPath;
201   }
202
203   @NotNull
204   public static String getDefaultPluginPathFor(@NotNull String selector) {
205     return platformPath(selector, "Library/Application Support", PLUGINS_FOLDER);
206   }
207
208   // runtime paths
209
210   @NotNull
211   public static String getSystemPath() {
212     if (ourSystemPath != null) return ourSystemPath;
213
214     if (System.getProperty(PROPERTY_SYSTEM_PATH) != null) {
215       ourSystemPath = getAbsolutePath(trimPathQuotes(System.getProperty(PROPERTY_SYSTEM_PATH)));
216     }
217     else if (PATHS_SELECTOR != null) {
218       ourSystemPath = platformPath(PATHS_SELECTOR, "Library/Caches", SYSTEM_FOLDER);
219     }
220     else {
221       ourSystemPath = getHomePath() + File.separator + SYSTEM_FOLDER;
222     }
223
224     checkAndCreate(ourSystemPath, true);
225     return ourSystemPath;
226   }
227
228   @NotNull
229   public static String getTempPath() {
230     return getSystemPath() + File.separator + "tmp";
231   }
232
233   @NotNull
234   public static File getIndexRoot() {
235     String indexRoot = System.getProperty("index_root_path", getSystemPath() + "/index");
236     checkAndCreate(indexRoot, true);
237     return new File(indexRoot);
238   }
239
240   @NotNull
241   public static String getLogPath() {
242     if (ourLogPath != null) return ourLogPath;
243
244     if (System.getProperty(PROPERTY_LOG_PATH) != null) {
245       ourLogPath = getAbsolutePath(trimPathQuotes(System.getProperty(PROPERTY_LOG_PATH)));
246     }
247     else if (SystemInfo.isMac && PATHS_SELECTOR != null) {
248       ourLogPath = getUserHome() + File.separator + "Library/Logs" + File.separator + PATHS_SELECTOR;
249     }
250     else {
251       ourLogPath = getSystemPath() + File.separatorChar + LOG_DIRECTORY;
252     }
253
254     return ourLogPath;
255   }
256
257   @NotNull
258   public static String getPluginTempPath () {
259     return getSystemPath() + File.separator + PLUGINS_FOLDER;
260   }
261
262   // misc stuff
263
264   /**
265    * Attempts to detect classpath entry which contains given resource.
266    */
267   @Nullable
268   public static String getResourceRoot(@NotNull Class context, String path) {
269     URL url = context.getResource(path);
270     if (url == null) {
271       url = ClassLoader.getSystemResource(path.substring(1));
272     }
273     return url != null ? extractRoot(url, path) : null;
274   }
275
276   /**
277    * Attempts to extract classpath entry part from passed URL.
278    */
279   @Nullable
280   private static String extractRoot(URL resourceURL, String resourcePath) {
281     if (!(StringUtil.startsWithChar(resourcePath, '/') || StringUtil.startsWithChar(resourcePath, '\\'))) {
282       log("precondition failed: " + resourcePath);
283       return null;
284     }
285
286     String resultPath = null;
287     String protocol = resourceURL.getProtocol();
288     if (URLUtil.FILE_PROTOCOL.equals(protocol)) {
289       String path = resourceURL.getFile();
290       String testPath = path.replace('\\', '/');
291       String testResourcePath = resourcePath.replace('\\', '/');
292       if (StringUtil.endsWithIgnoreCase(testPath, testResourcePath)) {
293         resultPath = path.substring(0, path.length() - resourcePath.length());
294       }
295     }
296     else if (URLUtil.JAR_PROTOCOL.equals(protocol)) {
297       Pair<String, String> paths = URLUtil.splitJarUrl(resourceURL.getFile());
298       if (paths != null) {
299         resultPath = paths.first;
300       }
301     }
302
303     if (resultPath == null) {
304       log("cannot extract: " + resourcePath + " from " + resourceURL);
305       return null;
306     }
307
308     resultPath = StringUtil.trimEnd(resultPath, File.separator);
309     resultPath = URLUtil.unescapePercentSequences(resultPath);
310
311     return resultPath;
312   }
313
314   public static void loadProperties() {
315     String[] propFiles = {
316       System.getProperty(PROPERTIES_FILE),
317       getUserPropertiesPath(),
318       getHomePath() + "/bin/idea.properties",
319       getHomePath() + "/community/bin/idea.properties"};
320
321     for (String path : propFiles) {
322       if (path != null) {
323         File propFile = new File(path);
324         if (propFile.exists()) {
325           try {
326             Reader fis = new BufferedReader(new FileReader(propFile));
327             try {
328               Map<String, String> properties = FileUtil.loadProperties(fis);
329
330               Properties sysProperties = System.getProperties();
331               for (String key : properties.keySet()) {
332                 if (PROPERTY_HOME_PATH.equals(key) || PROPERTY_HOME.equals(key)) {
333                   log(propFile.getPath() + ": '" + PROPERTY_HOME_PATH + "' and '" + PROPERTY_HOME + "' properties cannot be redefined");
334                 }
335                 else if (sysProperties.getProperty(key, null) != null) {
336                   log(propFile.getPath() + ": '" + key + "' already defined");
337                 }
338                 else {
339                   String value = substituteVars(properties.get(key));
340                   sysProperties.setProperty(key, value);
341                 }
342               }
343             }
344             finally {
345               fis.close();
346             }
347           }
348           catch (IOException e) {
349             log("Problem reading from property file: " + propFile.getPath());
350           }
351         }
352       }
353     }
354   }
355
356   private static String getUserPropertiesPath() {
357     if (PATHS_SELECTOR != null) {
358       // do not use getConfigPath() here - as it may be not yet defined
359       return platformPath(PATHS_SELECTOR, "Library/Preferences", /*"APPDATA", "XDG_CONFIG_HOME", ".config",*/ "") + "/idea.properties";
360     }
361     else {
362       return getUserHome() + "/idea.properties";
363     }
364   }
365
366   @Contract("null -> null")
367   public static String substituteVars(String s) {
368     return substituteVars(s, getHomePath());
369   }
370
371   @Contract("null, _ -> null")
372   public static String substituteVars(String s, String ideaHomePath) {
373     if (s == null) return null;
374
375     if (s.startsWith("..")) {
376       s = ideaHomePath + File.separatorChar + BIN_FOLDER + File.separatorChar + s;
377     }
378
379     Matcher m = PROPERTY_REF.matcher(s);
380     while (m.find()) {
381       String key = m.group(1);
382       String value = System.getProperty(key);
383
384       if (value == null) {
385         if (PROPERTY_HOME_PATH.equals(key) || PROPERTY_HOME.equals(key)) {
386           value = ideaHomePath;
387         }
388         else if (PROPERTY_CONFIG_PATH.equals(key)) {
389           value = getConfigPath();
390         }
391         else if (PROPERTY_SYSTEM_PATH.equals(key)) {
392           value = getSystemPath();
393         }
394       }
395
396       if (value == null) {
397         log("Unknown property: " + key);
398         value = "";
399       }
400
401       s = StringUtil.replace(s, m.group(), value);
402       m = PROPERTY_REF.matcher(s);
403     }
404
405     return s;
406   }
407
408   @NotNull
409   public static File findFileInLibDirectory(@NotNull String relativePath) {
410     File file = new File(getLibPath() + File.separator + relativePath);
411     return file.exists() ? file : new File(getHomePath(), "community" + File.separator + "lib" + File.separator + relativePath);
412   }
413
414   @Nullable
415   public static String getJarPathForClass(@NotNull Class aClass) {
416     String resourceRoot = getResourceRoot(aClass, "/" + aClass.getName().replace('.', '/') + ".class");
417     return resourceRoot != null ? new File(resourceRoot).getAbsolutePath() : null;
418   }
419
420   @NotNull
421   public static Collection<String> getUtilClassPath() {
422     final Class<?>[] classes = {
423       PathManager.class,            // module 'util'
424       NotNull.class,                // module 'annotations'
425       SystemInfoRt.class,           // module 'util-rt'
426       Document.class,               // jDOM
427       Appender.class,               // log4j
428       THashSet.class,               // trove4j
429       PicoContainer.class,          // PicoContainer
430       TypeMapper.class,             // JNA
431       FileUtils.class,              // JNA (jna-utils)
432       PatternMatcher.class          // OROMatcher
433     };
434
435     final Set<String> classPath = new HashSet<String>();
436     for (Class<?> aClass : classes) {
437       final String path = getJarPathForClass(aClass);
438       if (path != null) {
439         classPath.add(path);
440       }
441     }
442
443     final String resourceRoot = getResourceRoot(PathManager.class, "/messages/CommonBundle.properties");  // platform-resources-en
444     if (resourceRoot != null) {
445       classPath.add(new File(resourceRoot).getAbsolutePath());
446     }
447
448     return Collections.unmodifiableCollection(classPath);
449   }
450
451   // helpers
452
453   @SuppressWarnings("UseOfSystemOutOrSystemErr")
454   private static void log(String x) {
455     System.err.println(x);
456   }
457
458   private static String getAbsolutePath(String path) {
459     path = FileUtil.expandUserHome(path);
460     return FileUtil.toCanonicalPath(new File(path).getAbsolutePath());
461   }
462
463   private static String trimPathQuotes(String path){
464     if (!(path != null && !(path.length() < 3))){
465       return path;
466     }
467     if (StringUtil.startsWithChar(path, '\"') && StringUtil.endsWithChar(path, '\"')){
468       return path.substring(1, path.length() - 1);
469     }
470     return path;
471   }
472
473   // todo[r.sh] XDG directories, Windows folders
474   // http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
475   // http://www.microsoft.com/security/portal/mmpc/shared/variables.aspx
476   private static String platformPath(@NotNull String selector, @Nullable String macPart, @NotNull String fallback) {
477     return platformPath(selector, macPart, null, null, null, fallback);
478   }
479
480   private static String platformPath(@NotNull String selector,
481                                      @Nullable String macPart,
482                                      @Nullable String winVar,
483                                      @Nullable String xdgVar,
484                                      @Nullable String xdgDir,
485                                      @NotNull String fallback) {
486     if (macPart != null && SystemInfo.isMac) {
487       return getUserHome() + File.separator + macPart + File.separator + selector;
488     }
489
490     if (winVar != null && SystemInfo.isWindows) {
491       String dir = System.getenv(winVar);
492       if (dir != null) {
493         return dir + File.separator + selector;
494       }
495     }
496
497     if (xdgVar != null && xdgDir != null && SystemInfo.hasXdgOpen()) {
498       String dir = System.getenv(xdgVar);
499       if (dir == null) dir = getUserHome() + File.separator + xdgDir;
500       return dir + File.separator + selector;
501     }
502
503     return getUserHome() + File.separator + "." + selector + (!fallback.isEmpty() ? File.separator + fallback : "");
504   }
505
506   private static boolean checkAndCreate(String path, boolean createIfNotExists) {
507     if (createIfNotExists) {
508       File file = new File(path);
509       if (!file.exists()) {
510         return file.mkdirs();
511       }
512     }
513     return false;
514   }
515
516   // outdated stuff
517
518   /** @deprecated use {@link #getPluginsPath()} (to remove in IDEA 14) */
519   @SuppressWarnings("UnusedDeclaration") public static final String PLUGINS_DIRECTORY = PLUGINS_FOLDER;
520
521   /** @deprecated use {@link #getPreInstalledPluginsPath()} (to remove in IDEA 14) */
522   @SuppressWarnings({"UnusedDeclaration", "MethodNamesDifferingOnlyByCase", "SpellCheckingInspection"})
523   public static String getPreinstalledPluginsPath() {
524     return getPreInstalledPluginsPath();
525   }
526
527   /** @deprecated use {@link #getConfigPath()} (to remove in IDEA 14) */
528   @SuppressWarnings("UnusedDeclaration")
529   public static String getConfigPath(boolean createIfNotExists) {
530     ensureConfigFolderExists();
531     return ourConfigPath;
532   }
533
534   /** @deprecated use {@link #ensureConfigFolderExists()} (to remove in IDEA 14) */
535   @SuppressWarnings("UnusedDeclaration")
536   public static boolean ensureConfigFolderExists(boolean createIfNotExists) {
537     return checkAndCreate(getConfigPath(), createIfNotExists);
538   }
539
540   /** @deprecated use {@link #getOptionsPath()} (to remove in IDEA 14) */
541   @SuppressWarnings("UnusedDeclaration")
542   public static String getOptionsPathWithoutDialog() {
543     return getOptionsPath();
544   }
545
546   /** @deprecated use {@link #getOptionsFile(String)} (to remove in IDEA 14) */
547   @SuppressWarnings("UnusedDeclaration")
548   public static File getDefaultOptionsFile() {
549     return new File(getOptionsPath(), DEFAULT_OPTIONS_FILE_NAME + ".xml");
550   }
551 }