IDEA-135970 (adjusted path included in JDK validation)
[idea/community.git] / java / java-impl / src / com / intellij / openapi / projectRoots / impl / JavaSdkImpl.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.projectRoots.impl;
17
18 import com.intellij.execution.configurations.GeneralCommandLine;
19 import com.intellij.execution.util.ExecUtil;
20 import com.intellij.icons.AllIcons;
21 import com.intellij.lang.LangBundle;
22 import com.intellij.openapi.actionSystem.DataKey;
23 import com.intellij.openapi.application.PathManager;
24 import com.intellij.openapi.diagnostic.Logger;
25 import com.intellij.openapi.fileChooser.FileChooserDescriptor;
26 import com.intellij.openapi.project.ProjectBundle;
27 import com.intellij.openapi.projectRoots.*;
28 import com.intellij.openapi.roots.AnnotationOrderRootType;
29 import com.intellij.openapi.roots.JavadocOrderRootType;
30 import com.intellij.openapi.roots.OrderRootType;
31 import com.intellij.openapi.util.SystemInfo;
32 import com.intellij.openapi.util.io.FileUtil;
33 import com.intellij.openapi.util.text.StringUtil;
34 import com.intellij.openapi.vfs.*;
35 import com.intellij.openapi.vfs.impl.jrt.JrtFileSystem;
36 import com.intellij.util.Function;
37 import com.intellij.util.containers.ContainerUtil;
38 import com.intellij.util.containers.HashMap;
39 import org.jdom.Element;
40 import org.jetbrains.annotations.NotNull;
41 import org.jetbrains.annotations.Nullable;
42 import org.jetbrains.jps.model.java.impl.JavaSdkUtil;
43
44 import javax.swing.*;
45 import java.io.File;
46 import java.io.FileFilter;
47 import java.util.*;
48 import java.util.regex.Matcher;
49 import java.util.regex.Pattern;
50
51 /**
52  * @author Eugene Zhuravlev
53  * @since Sep 17, 2004
54  */
55 public class JavaSdkImpl extends JavaSdk {
56   private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.projectRoots.impl.JavaSdkImpl");
57
58   public static final DataKey<Boolean> KEY = DataKey.create("JavaSdk");
59
60   private static final String VM_EXE_NAME = "java";   // do not use JavaW.exe for Windows because of issues with encoding
61   private static final Pattern VERSION_STRING_PATTERN = Pattern.compile("^(.*)java version \"([1234567890_.]*)\"(.*)$");
62   private static final String JAVA_VERSION_PREFIX = "java version ";
63   private static final String OPENJDK_VERSION_PREFIX = "openjdk version ";
64
65   public JavaSdkImpl() {
66     super("JavaSDK");
67   }
68
69   @Override
70   public String getPresentableName() {
71     return ProjectBundle.message("sdk.java.name");
72   }
73
74   @Override
75   public Icon getIcon() {
76     return AllIcons.Nodes.PpJdk;
77   }
78
79   @NotNull
80   @Override
81   public String getHelpTopic() {
82     return "reference.project.structure.sdk.java";
83   }
84
85   @Override
86   public Icon getIconForAddAction() {
87     return AllIcons.General.AddJdk;
88   }
89
90   @Override
91   @Nullable
92   public String getDefaultDocumentationUrl(@NotNull final Sdk sdk) {
93     final JavaSdkVersion version = getVersion(sdk);
94     if (version == JavaSdkVersion.JDK_1_5) {
95       return "http://docs.oracle.com/javase/1.5.0/docs/api/";
96     }
97     if (version == JavaSdkVersion.JDK_1_6) {
98       return "http://docs.oracle.com/javase/6/docs/api/";
99     }
100     if (version == JavaSdkVersion.JDK_1_7) {
101       return "http://docs.oracle.com/javase/7/docs/api/";
102     }
103     if (version == JavaSdkVersion.JDK_1_8) {
104       return "http://docs.oracle.com/javase/8/docs/api";
105     }
106     return null;
107   }
108
109   @Override
110   public AdditionalDataConfigurable createAdditionalDataConfigurable(SdkModel sdkModel, SdkModificator sdkModificator) {
111     return null;
112   }
113
114   @Override
115   public void saveAdditionalData(@NotNull SdkAdditionalData additionalData, @NotNull Element additional) {
116   }
117
118   @Override
119   @SuppressWarnings({"HardCodedStringLiteral"})
120   public String getBinPath(@NotNull Sdk sdk) {
121     return getConvertedHomePath(sdk) + "bin";
122   }
123
124   @Override
125   public String getToolsPath(@NotNull Sdk sdk) {
126     final String versionString = sdk.getVersionString();
127     final boolean isJdk1_x = versionString != null && (versionString.contains("1.0") || versionString.contains("1.1"));
128     return getConvertedHomePath(sdk) + "lib" + File.separator + (isJdk1_x? "classes.zip" : "tools.jar");
129   }
130
131   @Override
132   public String getVMExecutablePath(@NotNull Sdk sdk) {
133     return getBinPath(sdk) + File.separator + VM_EXE_NAME;
134   }
135
136   private static String getConvertedHomePath(Sdk sdk) {
137     String homePath = sdk.getHomePath();
138     assert homePath != null : sdk;
139     String path = FileUtil.toSystemDependentName(homePath);
140     if (!path.endsWith(File.separator)) {
141       path += File.separator;
142     }
143     return path;
144   }
145
146   @Override
147   @SuppressWarnings({"HardCodedStringLiteral"})
148   public String suggestHomePath() {
149     if (SystemInfo.isMac) {
150       if (new File("/usr/libexec/java_home").canExecute()) {
151         String path = ExecUtil.execAndReadLine(new GeneralCommandLine("/usr/libexec/java_home"));
152         if (path != null && new File(path).exists()) {
153           return path;
154         }
155       }
156       return "/System/Library/Frameworks/JavaVM.framework/Versions";
157     }
158
159     if (SystemInfo.isLinux) {
160       final String[] homes = {"/usr/java", "/opt/java", "/usr/lib/jvm"};
161       for (String home : homes) {
162         if (new File(home).isDirectory()) {
163           return home;
164         }
165       }
166     }
167
168     if (SystemInfo.isSolaris) {
169       return "/usr/jdk";
170     }
171
172     if (SystemInfo.isWindows) {
173       String property = System.getProperty("java.home");
174       if (property == null) return null;
175       File javaHome = new File(property).getParentFile();//actually java.home points to to jre home
176       if (javaHome != null && JdkUtil.checkForJdk(javaHome)) {
177         return javaHome.getAbsolutePath();
178       }
179     }
180
181     return null;
182   }
183
184   @NotNull
185   @Override
186   public Collection<String> suggestHomePaths() {
187     if (!SystemInfo.isWindows)
188       return Collections.singletonList(suggestHomePath());
189
190     String property = System.getProperty("java.home");
191     if (property == null)
192       return Collections.emptyList();
193
194     File javaHome = new File(property).getParentFile();//actually java.home points to to jre home
195     if (javaHome == null || !javaHome.isDirectory() || javaHome.getParentFile() == null) {
196       return Collections.emptyList();
197     }
198     ArrayList<String> result = new ArrayList<String>();
199     File javasFolder = javaHome.getParentFile();
200     scanFolder(javasFolder, result);
201     File parentFile = javasFolder.getParentFile();
202     File root = parentFile != null ? parentFile.getParentFile() : null;
203     String name = parentFile != null ? parentFile.getName() : "";
204     if (name.contains("Program Files") && root != null) {
205       String x86Suffix = " (x86)";
206       boolean x86 = name.endsWith(x86Suffix) && name.length() > x86Suffix.length();
207       File anotherJavasFolder;
208       if (x86) {
209         anotherJavasFolder = new File(root, name.substring(0, name.length() - x86Suffix.length()));
210       }
211       else {
212         anotherJavasFolder = new File(root, name + x86Suffix);
213       }
214       if (anotherJavasFolder.isDirectory()) {
215         scanFolder(new File(anotherJavasFolder, javasFolder.getName()), result);
216       }
217     }
218     return result;
219   }
220
221   private static void scanFolder(File javasFolder, ArrayList<String> result) {
222     File[] candidates = javasFolder.listFiles(new FileFilter() {
223       @Override
224       public boolean accept(File pathname) {
225         return JdkUtil.checkForJdk(pathname);
226       }
227     });
228     if (candidates != null) {
229       result.addAll(ContainerUtil.map2List(candidates, new Function<File, String>() {
230         @Override
231         public String fun(File file) {
232           return file.getAbsolutePath();
233         }
234       }));
235     }
236   }
237
238   @Override
239   public FileChooserDescriptor getHomeChooserDescriptor() {
240     final FileChooserDescriptor baseDescriptor = super.getHomeChooserDescriptor();
241     final FileChooserDescriptor descriptor = new FileChooserDescriptor(baseDescriptor) {
242       @Override
243       public void validateSelectedFiles(VirtualFile[] files) throws Exception {
244         if (files.length > 0 && !JrtFileSystem.isSupported()) {
245           String path = files[0].getPath();
246           if (JrtFileSystem.isModularJdk(path) || JrtFileSystem.isModularJdk(adjustSelectedSdkHome(path))) {
247             throw new Exception(LangBundle.message("jrt.not.available.message"));
248           }
249         }
250         baseDescriptor.validateSelectedFiles(files);
251       }
252     };
253     descriptor.putUserData(KEY, Boolean.TRUE);
254     return descriptor;
255   }
256
257   @Override
258   public String adjustSelectedSdkHome(String homePath) {
259     if (SystemInfo.isMac) {
260       File home = new File(homePath, "/Home");
261       if (home.exists()) return home.getPath();
262
263       home = new File(homePath, "Contents/Home");
264       if (home.exists()) return home.getPath();
265     }
266
267     return homePath;
268   }
269
270   @Override
271   public boolean isValidSdkHome(String path) {
272     if (!checkForJdk(new File(path))) {
273       return false;
274     }
275     if (JrtFileSystem.isModularJdk(path) && !JrtFileSystem.isSupported()) {
276       return false;
277     }
278     return true;
279   }
280
281   @Override
282   public String suggestSdkName(String currentSdkName, String sdkHome) {
283     final String suggestedName;
284     if (currentSdkName != null && !currentSdkName.isEmpty()) {
285       final Matcher matcher = VERSION_STRING_PATTERN.matcher(currentSdkName);
286       final boolean replaceNameWithVersion = matcher.matches();
287       if (replaceNameWithVersion){
288         // user did not change name -> set it automatically
289         final String versionString = getVersionString(sdkHome);
290         suggestedName = versionString == null ? currentSdkName : matcher.replaceFirst("$1" + versionString + "$3");
291       }
292       else {
293         suggestedName = currentSdkName;
294       }
295     }
296     else {
297       String versionString = getVersionString(sdkHome);
298       suggestedName = versionString == null ? ProjectBundle.message("sdk.java.unknown.name") : getVersionNumber(versionString);
299     }
300     return suggestedName;
301   }
302
303   @NotNull
304   private static String getVersionNumber(@NotNull String versionString) {
305     if (versionString.startsWith(JAVA_VERSION_PREFIX) || versionString.startsWith(OPENJDK_VERSION_PREFIX)) {
306       boolean openJdk = versionString.startsWith(OPENJDK_VERSION_PREFIX);
307       versionString = versionString.substring(openJdk ? OPENJDK_VERSION_PREFIX.length() : JAVA_VERSION_PREFIX.length());
308       if (versionString.startsWith("\"") && versionString.endsWith("\"")) {
309         versionString = versionString.substring(1, versionString.length() - 1);
310       }
311       int dotIdx = versionString.indexOf('.');
312       if (dotIdx > 0) {
313         try {
314           int major = Integer.parseInt(versionString.substring(0, dotIdx));
315           int minorDot = versionString.indexOf('.', dotIdx + 1);
316           if (minorDot > 0) {
317             int minor = Integer.parseInt(versionString.substring(dotIdx + 1, minorDot));
318             versionString = major + "." + minor;
319           }
320         }
321         catch (NumberFormatException e) {
322           // Do nothing. Use original version string if failed to parse according to major.minor pattern.
323         }
324       }
325     }
326     return versionString;
327   }
328
329   @Override
330   @SuppressWarnings({"HardCodedStringLiteral"})
331   public void setupSdkPaths(@NotNull Sdk sdk) {
332     String homePath = sdk.getHomePath();
333     assert homePath != null : sdk;
334
335     File jdkHome = new File(homePath);
336     List<VirtualFile> classes = findClasses(jdkHome, false);
337     VirtualFile sources = findSources(jdkHome);
338     VirtualFile docs = findDocs(jdkHome, "docs/api");
339     SdkModificator sdkModificator = sdk.getSdkModificator();
340
341     Set<VirtualFile> previousRoots = new LinkedHashSet<VirtualFile>(Arrays.asList(sdkModificator.getRoots(OrderRootType.CLASSES)));
342     sdkModificator.removeRoots(OrderRootType.CLASSES);
343     previousRoots.removeAll(new HashSet<VirtualFile>(classes));
344     for (VirtualFile aClass : classes) {
345       sdkModificator.addRoot(aClass, OrderRootType.CLASSES);
346     }
347     for (VirtualFile root : previousRoots) {
348       sdkModificator.addRoot(root, OrderRootType.CLASSES);
349     }
350
351     if (sources != null) {
352       sdkModificator.addRoot(sources, OrderRootType.SOURCES);
353     }
354     VirtualFile javaFxSources = findSources(jdkHome, "javafx-src");
355     if (javaFxSources != null) {
356       sdkModificator.addRoot(javaFxSources, OrderRootType.SOURCES);
357     }
358
359     if (docs != null) {
360       sdkModificator.addRoot(docs, JavadocOrderRootType.getInstance());
361     }
362     else if (SystemInfo.isMac) {
363       VirtualFile commonDocs = findDocs(jdkHome, "docs");
364       if (commonDocs == null) {
365         commonDocs = findInJar(new File(jdkHome, "docs.jar"), "doc/api");
366         if (commonDocs == null) {
367           commonDocs = findInJar(new File(jdkHome, "docs.jar"), "docs/api");
368         }
369       }
370       if (commonDocs != null) {
371         sdkModificator.addRoot(commonDocs, JavadocOrderRootType.getInstance());
372       }
373
374       VirtualFile appleDocs = findDocs(jdkHome, "appledocs");
375       if (appleDocs == null) {
376         appleDocs = findInJar(new File(jdkHome, "appledocs.jar"), "appledoc/api");
377       }
378       if (appleDocs != null) {
379         sdkModificator.addRoot(appleDocs, JavadocOrderRootType.getInstance());
380       }
381
382       if (commonDocs == null && appleDocs == null && sources == null) {
383         String url = getDefaultDocumentationUrl(sdk);
384         if (url != null) {
385           sdkModificator.addRoot(VirtualFileManager.getInstance().findFileByUrl(url), JavadocOrderRootType.getInstance());
386         }
387       }
388     }
389     else if (getVersion(sdk) == JavaSdkVersion.JDK_1_7) {
390       VirtualFile url = VirtualFileManager.getInstance().findFileByUrl("http://docs.oracle.com/javafx/2/api/");
391       sdkModificator.addRoot(url, JavadocOrderRootType.getInstance());
392     }
393
394     attachJdkAnnotations(sdkModificator);
395
396     sdkModificator.commitChanges();
397   }
398
399   public static void attachJdkAnnotations(@NotNull SdkModificator modificator) {
400     LocalFileSystem lfs = LocalFileSystem.getInstance();
401     // community idea under idea
402     VirtualFile root = lfs.findFileByPath(FileUtil.toSystemIndependentName(PathManager.getHomePath()) + "/java/jdkAnnotations");
403
404     if (root == null) {  // idea under idea
405       root = lfs.findFileByPath(FileUtil.toSystemIndependentName(PathManager.getHomePath()) + "/community/java/jdkAnnotations");
406     }
407     if (root == null) { // build
408       root = VirtualFileManager.getInstance().findFileByUrl("jar://"+ FileUtil.toSystemIndependentName(PathManager.getHomePath()) + "/lib/jdkAnnotations.jar!/");
409     }
410     if (root == null) {
411       LOG.error("jdk annotations not found in: "+ FileUtil.toSystemIndependentName(PathManager.getHomePath()) + "/lib/jdkAnnotations.jar!/");
412       return;
413     }
414
415     OrderRootType annoType = AnnotationOrderRootType.getInstance();
416     modificator.removeRoot(root, annoType);
417     modificator.addRoot(root, annoType);
418   }
419
420   private final Map<String, String> myCachedVersionStrings = new HashMap<String, String>();
421
422   @Override
423   public final String getVersionString(String sdkHome) {
424     String versionString = myCachedVersionStrings.get(sdkHome);
425     if (versionString == null) {
426       versionString = getJdkVersion(sdkHome);
427       if (!StringUtil.isEmpty(versionString)) {
428         myCachedVersionStrings.put(sdkHome, versionString);
429       }
430     }
431     return versionString;
432   }
433
434   @Override
435   public int compareTo(@NotNull String versionString, @NotNull String versionNumber) {
436     return getVersionNumber(versionString).compareTo(versionNumber);
437   }
438
439   @Override
440   public JavaSdkVersion getVersion(@NotNull Sdk sdk) {
441     String version = sdk.getVersionString();
442     if (version == null) return null;
443     return JdkVersionUtil.getVersion(version);
444   }
445
446   @Override
447   @Nullable
448   public JavaSdkVersion getVersion(@NotNull String versionString) {
449     return JdkVersionUtil.getVersion(versionString);
450   }
451
452   @Override
453   public boolean isOfVersionOrHigher(@NotNull Sdk sdk, @NotNull JavaSdkVersion version) {
454     JavaSdkVersion sdkVersion = getVersion(sdk);
455     return sdkVersion != null && sdkVersion.isAtLeast(version);
456   }
457
458   @Override
459   public Sdk createJdk(@NotNull String jdkName, @NotNull String home, boolean isJre) {
460     ProjectJdkImpl jdk = new ProjectJdkImpl(jdkName, this);
461     SdkModificator sdkModificator = jdk.getSdkModificator();
462
463     String path = home.replace(File.separatorChar, '/');
464     sdkModificator.setHomePath(path);
465     sdkModificator.setVersionString(jdkName); // must be set after home path, otherwise setting home path clears the version string
466
467     File jdkHomeFile = new File(home);
468     addClasses(jdkHomeFile, sdkModificator, isJre);
469     addSources(jdkHomeFile, sdkModificator);
470     addDocs(jdkHomeFile, sdkModificator);
471     sdkModificator.commitChanges();
472
473     return jdk;
474   }
475
476   private static void addClasses(File file, SdkModificator sdkModificator, boolean isJre) {
477     for (VirtualFile virtualFile : findClasses(file, isJre)) {
478       sdkModificator.addRoot(virtualFile, OrderRootType.CLASSES);
479     }
480   }
481
482   private static List<VirtualFile> findClasses(File file, boolean isJre) {
483     List<File> roots = JavaSdkUtil.getJdkClassesRoots(file, isJre);
484     List<String> urls = ContainerUtil.newArrayListWithCapacity(roots.size() + 1);
485     if (JrtFileSystem.isModularJdk(file.getPath())) {
486       urls.add(VirtualFileManager.constructUrl(JrtFileSystem.PROTOCOL, FileUtil.toSystemIndependentName(file.getPath()) + JrtFileSystem.SEPARATOR));
487     }
488     for (File root : roots) {
489       urls.add(VfsUtil.getUrlForLibraryRoot(root));
490     }
491
492     List<VirtualFile> result = ContainerUtil.newArrayListWithCapacity(urls.size());
493     for (String url : urls) {
494       VirtualFile vFile = VirtualFileManager.getInstance().findFileByUrl(url);
495       if (vFile != null) {
496         result.add(vFile);
497       }
498     }
499     return result;
500   }
501
502   private static void addSources(File file, SdkModificator sdkModificator) {
503     VirtualFile vFile = findSources(file);
504     if (vFile != null) {
505       sdkModificator.addRoot(vFile, OrderRootType.SOURCES);
506     }
507   }
508
509   @Nullable
510   @SuppressWarnings({"HardCodedStringLiteral"})
511   public static VirtualFile findSources(File file) {
512     return findSources(file, "src");
513   }
514
515   @Nullable
516   @SuppressWarnings({"HardCodedStringLiteral"})
517   public static VirtualFile findSources(File file, final String srcName) {
518     File srcDir = new File(file, "src");
519     File jarFile = new File(file, srcName + ".jar");
520     if (!jarFile.exists()) {
521       jarFile = new File(file, srcName + ".zip");
522     }
523
524     if (jarFile.exists()) {
525       VirtualFile vFile = findInJar(jarFile, "src");
526       if (vFile != null) return vFile;
527       // try 1.4 format
528       vFile = findInJar(jarFile, "");
529       return vFile;
530     }
531     else {
532       if (!srcDir.exists() || !srcDir.isDirectory()) return null;
533       String path = srcDir.getAbsolutePath().replace(File.separatorChar, '/');
534       return LocalFileSystem.getInstance().findFileByPath(path);
535     }
536   }
537
538   @SuppressWarnings({"HardCodedStringLiteral"})
539   private static void addDocs(File file, SdkModificator rootContainer) {
540     VirtualFile vFile = findDocs(file, "docs/api");
541     if (vFile != null) {
542       rootContainer.addRoot(vFile, JavadocOrderRootType.getInstance());
543     }
544   }
545
546   @Nullable
547   private static VirtualFile findInJar(File jarFile, String relativePath) {
548     if (!jarFile.exists()) return null;
549     String url = JarFileSystem.PROTOCOL_PREFIX +
550                  jarFile.getAbsolutePath().replace(File.separatorChar, '/') + JarFileSystem.JAR_SEPARATOR + relativePath;
551     return VirtualFileManager.getInstance().findFileByUrl(url);
552   }
553
554   @Nullable
555   public static VirtualFile findDocs(File file, final String relativePath) {
556     file = new File(file.getAbsolutePath() + File.separator + relativePath.replace('/', File.separatorChar));
557     if (!file.exists() || !file.isDirectory()) return null;
558     String path = file.getAbsolutePath().replace(File.separatorChar, '/');
559     return LocalFileSystem.getInstance().findFileByPath(path);
560   }
561
562   @Override
563   public boolean isRootTypeApplicable(OrderRootType type) {
564     return type == OrderRootType.CLASSES ||
565            type == OrderRootType.SOURCES ||
566            type == JavadocOrderRootType.getInstance() ||
567            type == AnnotationOrderRootType.getInstance();
568   }
569 }