dbc4330202a8aef55d32dbe5a26314772ba3bad0
[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.isModularJdk(files[0].getPath()) && !JrtFileSystem.isSupported()) {
245           throw new Exception(LangBundle.message("jrt.not.available.message"));
246         }
247         baseDescriptor.validateSelectedFiles(files);
248       }
249     };
250     descriptor.putUserData(KEY, Boolean.TRUE);
251     return descriptor;
252   }
253
254   @Override
255   public String adjustSelectedSdkHome(String homePath) {
256     if (SystemInfo.isMac) {
257       File home = new File(homePath, "/Home");
258       if (home.exists()) return home.getPath();
259
260       home = new File(homePath, "Contents/Home");
261       if (home.exists()) return home.getPath();
262     }
263
264     return homePath;
265   }
266
267   @Override
268   public boolean isValidSdkHome(String path) {
269     if (!checkForJdk(new File(path))) {
270       return false;
271     }
272     if (JrtFileSystem.isModularJdk(path) && !JrtFileSystem.isSupported()) {
273       return false;
274     }
275     return true;
276   }
277
278   @Override
279   public String suggestSdkName(String currentSdkName, String sdkHome) {
280     final String suggestedName;
281     if (currentSdkName != null && !currentSdkName.isEmpty()) {
282       final Matcher matcher = VERSION_STRING_PATTERN.matcher(currentSdkName);
283       final boolean replaceNameWithVersion = matcher.matches();
284       if (replaceNameWithVersion){
285         // user did not change name -> set it automatically
286         final String versionString = getVersionString(sdkHome);
287         suggestedName = versionString == null ? currentSdkName : matcher.replaceFirst("$1" + versionString + "$3");
288       }
289       else {
290         suggestedName = currentSdkName;
291       }
292     }
293     else {
294       String versionString = getVersionString(sdkHome);
295       suggestedName = versionString == null ? ProjectBundle.message("sdk.java.unknown.name") : getVersionNumber(versionString);
296     }
297     return suggestedName;
298   }
299
300   @NotNull
301   private static String getVersionNumber(@NotNull String versionString) {
302     if (versionString.startsWith(JAVA_VERSION_PREFIX) || versionString.startsWith(OPENJDK_VERSION_PREFIX)) {
303       boolean openJdk = versionString.startsWith(OPENJDK_VERSION_PREFIX);
304       versionString = versionString.substring(openJdk ? OPENJDK_VERSION_PREFIX.length() : JAVA_VERSION_PREFIX.length());
305       if (versionString.startsWith("\"") && versionString.endsWith("\"")) {
306         versionString = versionString.substring(1, versionString.length() - 1);
307       }
308       int dotIdx = versionString.indexOf('.');
309       if (dotIdx > 0) {
310         try {
311           int major = Integer.parseInt(versionString.substring(0, dotIdx));
312           int minorDot = versionString.indexOf('.', dotIdx + 1);
313           if (minorDot > 0) {
314             int minor = Integer.parseInt(versionString.substring(dotIdx + 1, minorDot));
315             versionString = major + "." + minor;
316           }
317         }
318         catch (NumberFormatException e) {
319           // Do nothing. Use original version string if failed to parse according to major.minor pattern.
320         }
321       }
322     }
323     return versionString;
324   }
325
326   @Override
327   @SuppressWarnings({"HardCodedStringLiteral"})
328   public void setupSdkPaths(@NotNull Sdk sdk) {
329     String homePath = sdk.getHomePath();
330     assert homePath != null : sdk;
331
332     File jdkHome = new File(homePath);
333     List<VirtualFile> classes = findClasses(jdkHome, false);
334     VirtualFile sources = findSources(jdkHome);
335     VirtualFile docs = findDocs(jdkHome, "docs/api");
336     SdkModificator sdkModificator = sdk.getSdkModificator();
337
338     Set<VirtualFile> previousRoots = new LinkedHashSet<VirtualFile>(Arrays.asList(sdkModificator.getRoots(OrderRootType.CLASSES)));
339     sdkModificator.removeRoots(OrderRootType.CLASSES);
340     previousRoots.removeAll(new HashSet<VirtualFile>(classes));
341     for (VirtualFile aClass : classes) {
342       sdkModificator.addRoot(aClass, OrderRootType.CLASSES);
343     }
344     for (VirtualFile root : previousRoots) {
345       sdkModificator.addRoot(root, OrderRootType.CLASSES);
346     }
347
348     if (sources != null) {
349       sdkModificator.addRoot(sources, OrderRootType.SOURCES);
350     }
351     VirtualFile javaFxSources = findSources(jdkHome, "javafx-src");
352     if (javaFxSources != null) {
353       sdkModificator.addRoot(javaFxSources, OrderRootType.SOURCES);
354     }
355
356     if (docs != null) {
357       sdkModificator.addRoot(docs, JavadocOrderRootType.getInstance());
358     }
359     else if (SystemInfo.isMac) {
360       VirtualFile commonDocs = findDocs(jdkHome, "docs");
361       if (commonDocs == null) {
362         commonDocs = findInJar(new File(jdkHome, "docs.jar"), "doc/api");
363         if (commonDocs == null) {
364           commonDocs = findInJar(new File(jdkHome, "docs.jar"), "docs/api");
365         }
366       }
367       if (commonDocs != null) {
368         sdkModificator.addRoot(commonDocs, JavadocOrderRootType.getInstance());
369       }
370
371       VirtualFile appleDocs = findDocs(jdkHome, "appledocs");
372       if (appleDocs == null) {
373         appleDocs = findInJar(new File(jdkHome, "appledocs.jar"), "appledoc/api");
374       }
375       if (appleDocs != null) {
376         sdkModificator.addRoot(appleDocs, JavadocOrderRootType.getInstance());
377       }
378
379       if (commonDocs == null && appleDocs == null && sources == null) {
380         String url = getDefaultDocumentationUrl(sdk);
381         if (url != null) {
382           sdkModificator.addRoot(VirtualFileManager.getInstance().findFileByUrl(url), JavadocOrderRootType.getInstance());
383         }
384       }
385     }
386     else if (getVersion(sdk) == JavaSdkVersion.JDK_1_7) {
387       VirtualFile url = VirtualFileManager.getInstance().findFileByUrl("http://docs.oracle.com/javafx/2/api/");
388       sdkModificator.addRoot(url, JavadocOrderRootType.getInstance());
389     }
390
391     attachJdkAnnotations(sdkModificator);
392
393     sdkModificator.commitChanges();
394   }
395
396   public static void attachJdkAnnotations(@NotNull SdkModificator modificator) {
397     LocalFileSystem lfs = LocalFileSystem.getInstance();
398     // community idea under idea
399     VirtualFile root = lfs.findFileByPath(FileUtil.toSystemIndependentName(PathManager.getHomePath()) + "/java/jdkAnnotations");
400
401     if (root == null) {  // idea under idea
402       root = lfs.findFileByPath(FileUtil.toSystemIndependentName(PathManager.getHomePath()) + "/community/java/jdkAnnotations");
403     }
404     if (root == null) { // build
405       root = VirtualFileManager.getInstance().findFileByUrl("jar://"+ FileUtil.toSystemIndependentName(PathManager.getHomePath()) + "/lib/jdkAnnotations.jar!/");
406     }
407     if (root == null) {
408       LOG.error("jdk annotations not found in: "+ FileUtil.toSystemIndependentName(PathManager.getHomePath()) + "/lib/jdkAnnotations.jar!/");
409       return;
410     }
411
412     OrderRootType annoType = AnnotationOrderRootType.getInstance();
413     modificator.removeRoot(root, annoType);
414     modificator.addRoot(root, annoType);
415   }
416
417   private final Map<String, String> myCachedVersionStrings = new HashMap<String, String>();
418
419   @Override
420   public final String getVersionString(String sdkHome) {
421     String versionString = myCachedVersionStrings.get(sdkHome);
422     if (versionString == null) {
423       versionString = getJdkVersion(sdkHome);
424       if (!StringUtil.isEmpty(versionString)) {
425         myCachedVersionStrings.put(sdkHome, versionString);
426       }
427     }
428     return versionString;
429   }
430
431   @Override
432   public int compareTo(@NotNull String versionString, @NotNull String versionNumber) {
433     return getVersionNumber(versionString).compareTo(versionNumber);
434   }
435
436   @Override
437   public JavaSdkVersion getVersion(@NotNull Sdk sdk) {
438     String version = sdk.getVersionString();
439     if (version == null) return null;
440     return JdkVersionUtil.getVersion(version);
441   }
442
443   @Override
444   @Nullable
445   public JavaSdkVersion getVersion(@NotNull String versionString) {
446     return JdkVersionUtil.getVersion(versionString);
447   }
448
449   @Override
450   public boolean isOfVersionOrHigher(@NotNull Sdk sdk, @NotNull JavaSdkVersion version) {
451     JavaSdkVersion sdkVersion = getVersion(sdk);
452     return sdkVersion != null && sdkVersion.isAtLeast(version);
453   }
454
455   @Override
456   public Sdk createJdk(@NotNull String jdkName, @NotNull String home, boolean isJre) {
457     ProjectJdkImpl jdk = new ProjectJdkImpl(jdkName, this);
458     SdkModificator sdkModificator = jdk.getSdkModificator();
459
460     String path = home.replace(File.separatorChar, '/');
461     sdkModificator.setHomePath(path);
462     sdkModificator.setVersionString(jdkName); // must be set after home path, otherwise setting home path clears the version string
463
464     File jdkHomeFile = new File(home);
465     addClasses(jdkHomeFile, sdkModificator, isJre);
466     addSources(jdkHomeFile, sdkModificator);
467     addDocs(jdkHomeFile, sdkModificator);
468     sdkModificator.commitChanges();
469
470     return jdk;
471   }
472
473   private static void addClasses(File file, SdkModificator sdkModificator, boolean isJre) {
474     for (VirtualFile virtualFile : findClasses(file, isJre)) {
475       sdkModificator.addRoot(virtualFile, OrderRootType.CLASSES);
476     }
477   }
478
479   private static List<VirtualFile> findClasses(File file, boolean isJre) {
480     List<File> roots = JavaSdkUtil.getJdkClassesRoots(file, isJre);
481     List<String> urls = ContainerUtil.newArrayListWithCapacity(roots.size() + 1);
482     if (JrtFileSystem.isModularJdk(file.getPath())) {
483       urls.add(VirtualFileManager.constructUrl(JrtFileSystem.PROTOCOL, FileUtil.toSystemIndependentName(file.getPath()) + JrtFileSystem.SEPARATOR));
484     }
485     for (File root : roots) {
486       urls.add(VfsUtil.getUrlForLibraryRoot(root));
487     }
488
489     List<VirtualFile> result = ContainerUtil.newArrayListWithCapacity(urls.size());
490     for (String url : urls) {
491       VirtualFile vFile = VirtualFileManager.getInstance().findFileByUrl(url);
492       if (vFile != null) {
493         result.add(vFile);
494       }
495     }
496     return result;
497   }
498
499   private static void addSources(File file, SdkModificator sdkModificator) {
500     VirtualFile vFile = findSources(file);
501     if (vFile != null) {
502       sdkModificator.addRoot(vFile, OrderRootType.SOURCES);
503     }
504   }
505
506   @Nullable
507   @SuppressWarnings({"HardCodedStringLiteral"})
508   public static VirtualFile findSources(File file) {
509     return findSources(file, "src");
510   }
511
512   @Nullable
513   @SuppressWarnings({"HardCodedStringLiteral"})
514   public static VirtualFile findSources(File file, final String srcName) {
515     File srcDir = new File(file, "src");
516     File jarFile = new File(file, srcName + ".jar");
517     if (!jarFile.exists()) {
518       jarFile = new File(file, srcName + ".zip");
519     }
520
521     if (jarFile.exists()) {
522       VirtualFile vFile = findInJar(jarFile, "src");
523       if (vFile != null) return vFile;
524       // try 1.4 format
525       vFile = findInJar(jarFile, "");
526       return vFile;
527     }
528     else {
529       if (!srcDir.exists() || !srcDir.isDirectory()) return null;
530       String path = srcDir.getAbsolutePath().replace(File.separatorChar, '/');
531       return LocalFileSystem.getInstance().findFileByPath(path);
532     }
533   }
534
535   @SuppressWarnings({"HardCodedStringLiteral"})
536   private static void addDocs(File file, SdkModificator rootContainer) {
537     VirtualFile vFile = findDocs(file, "docs/api");
538     if (vFile != null) {
539       rootContainer.addRoot(vFile, JavadocOrderRootType.getInstance());
540     }
541   }
542
543   @Nullable
544   private static VirtualFile findInJar(File jarFile, String relativePath) {
545     if (!jarFile.exists()) return null;
546     String url = JarFileSystem.PROTOCOL_PREFIX +
547                  jarFile.getAbsolutePath().replace(File.separatorChar, '/') + JarFileSystem.JAR_SEPARATOR + relativePath;
548     return VirtualFileManager.getInstance().findFileByUrl(url);
549   }
550
551   @Nullable
552   public static VirtualFile findDocs(File file, final String relativePath) {
553     file = new File(file.getAbsolutePath() + File.separator + relativePath.replace('/', File.separatorChar));
554     if (!file.exists() || !file.isDirectory()) return null;
555     String path = file.getAbsolutePath().replace(File.separatorChar, '/');
556     return LocalFileSystem.getInstance().findFileByPath(path);
557   }
558
559   @Override
560   public boolean isRootTypeApplicable(OrderRootType type) {
561     return type == OrderRootType.CLASSES ||
562            type == OrderRootType.SOURCES ||
563            type == JavadocOrderRootType.getInstance() ||
564            type == AnnotationOrderRootType.getInstance();
565   }
566 }