Merge branch 'slava/plugin-incompatible-with'
[idea/community.git] / platform / core-impl / src / com / intellij / ide / plugins / IdeaPluginDescriptorImpl.java
1 // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.ide.plugins;
3
4 import com.intellij.AbstractBundle;
5 import com.intellij.DynamicBundle;
6 import com.intellij.openapi.components.ComponentConfig;
7 import com.intellij.openapi.extensions.PluginId;
8 import com.intellij.openapi.extensions.impl.ExtensionsAreaImpl;
9 import com.intellij.openapi.util.JDOMUtil;
10 import com.intellij.openapi.util.io.FileUtil;
11 import com.intellij.openapi.util.text.StringUtil;
12 import com.intellij.openapi.util.text.StringUtilRt;
13 import com.intellij.util.containers.ContainerUtil;
14 import com.intellij.util.ref.GCWatcher;
15 import org.jdom.Content;
16 import org.jdom.Element;
17 import org.jetbrains.annotations.ApiStatus;
18 import org.jetbrains.annotations.NotNull;
19 import org.jetbrains.annotations.Nullable;
20 import org.jetbrains.annotations.TestOnly;
21
22 import java.io.File;
23 import java.io.IOException;
24 import java.nio.file.DirectoryStream;
25 import java.nio.file.Files;
26 import java.nio.file.NoSuchFileException;
27 import java.nio.file.Path;
28 import java.text.ParseException;
29 import java.util.*;
30 import java.util.regex.Matcher;
31 import java.util.regex.Pattern;
32
33 @ApiStatus.Internal
34 public final class IdeaPluginDescriptorImpl implements IdeaPluginDescriptor {
35   public enum OS {
36     mac, linux, windows, unix, freebsd
37   }
38
39   public static final IdeaPluginDescriptorImpl[] EMPTY_ARRAY = new IdeaPluginDescriptorImpl[0];
40
41   final Path path;
42   // base path for resolving optional dependency descriptors
43   final Path basePath;
44
45   private final boolean myBundled;
46   String myName;
47   PluginId myId;
48   private volatile String myDescription;
49   private @Nullable String myProductCode;
50   private @Nullable Date myReleaseDate;
51   private int myReleaseVersion;
52   private boolean myIsLicenseOptional;
53   private String myResourceBundleBaseName;
54   private String myChangeNotes;
55   private String myVersion;
56   private String myVendor;
57   private String myVendorEmail;
58   private String myVendorUrl;
59   private String myCategory;
60   String myUrl;
61   @Nullable List<PluginDependency> pluginDependencies;
62   @Nullable private List<PluginId> myIncompatibilities;
63
64   transient List<Path> jarFiles;
65
66   private @Nullable List<Element> myActionElements;
67   // extension point name -> list of extension elements
68   // LinkedHashMap for predictable register order
69   private @Nullable LinkedHashMap<String, List<Element>> epNameToExtensionElements;
70
71   final ContainerDescriptor appContainerDescriptor = new ContainerDescriptor();
72   final ContainerDescriptor projectContainerDescriptor = new ContainerDescriptor();
73   final ContainerDescriptor moduleContainerDescriptor = new ContainerDescriptor();
74
75   private List<PluginId> myModules;
76   private ClassLoader myLoader;
77   private String myDescriptionChildText;
78   boolean myUseIdeaClassLoader;
79   private boolean myUseCoreClassLoader;
80   boolean myAllowBundledUpdate;
81   boolean myImplementationDetail;
82   boolean myRequireRestart;
83   private String mySinceBuild;
84   private String myUntilBuild;
85
86   private boolean myEnabled = true;
87   private boolean myDeleted;
88   private boolean isExtensionsCleared;
89
90   boolean incomplete;
91
92   public IdeaPluginDescriptorImpl(@NotNull Path path, @NotNull Path basePath, boolean bundled) {
93     this.path = path;
94     this.basePath = basePath;
95     myBundled = bundled;
96   }
97
98   @ApiStatus.Internal
99   public @NotNull ContainerDescriptor getApp() {
100     return appContainerDescriptor;
101   }
102
103   @ApiStatus.Internal
104   public @NotNull ContainerDescriptor getProject() {
105     return projectContainerDescriptor;
106   }
107
108   @ApiStatus.Internal
109   public @NotNull ContainerDescriptor getModule() {
110     return moduleContainerDescriptor;
111   }
112
113   @ApiStatus.Internal
114   public @NotNull List<PluginDependency> getPluginDependencies() {
115     return pluginDependencies == null ? Collections.emptyList() : pluginDependencies;
116   }
117
118   @Override
119   public @NotNull Path getPluginPath() {
120     return path;
121   }
122
123   boolean readExternal(@NotNull Element element,
124                        @NotNull PathBasedJdomXIncluder.PathResolver<?> pathResolver,
125                        @NotNull DescriptorListLoadingContext context,
126                        @NotNull IdeaPluginDescriptorImpl mainDescriptor) {
127     // root element always `!isIncludeElement`, and it means that result always is a singleton list
128     // (also, plugin xml describes one plugin, this descriptor is not able to represent several plugins)
129     if (JDOMUtil.isEmpty(element)) {
130       markAsIncomplete(context, "Empty plugin descriptor", null);
131       return false;
132     }
133
134     XmlReader.readIdAndName(this, element);
135
136     if (myId != null && context.isPluginDisabled(myId)) {
137       markAsIncomplete(context, null, null);
138     }
139     else {
140       PathBasedJdomXIncluder.resolveNonXIncludeElement(element, basePath, context, pathResolver);
141       if (myId == null || myName == null) {
142         // read again after resolve
143         XmlReader.readIdAndName(this, element);
144
145         if (myId != null && context.isPluginDisabled(myId)) {
146           markAsIncomplete(context, null, null);
147         }
148       }
149     }
150
151     if (incomplete) {
152       myDescriptionChildText = element.getChildTextTrim("description");
153       myCategory = element.getChildTextTrim("category");
154       myVersion = element.getChildTextTrim("version");
155       if (context.getLogger().isDebugEnabled()) {
156         context.getLogger().debug("Skipping reading of " + myId + " from " + basePath + " (reason: disabled)");
157       }
158       List<Element> dependsElements = element.getChildren("depends");
159       for (Element dependsElement : dependsElements) {
160         readPluginDependency(basePath, context, dependsElement);
161       }
162       Element productElement = element.getChild("product-descriptor");
163       if (productElement != null) {
164         readProduct(context, productElement);
165       }
166       return false;
167     }
168
169     XmlReader.readMetaInfo(this, element);
170
171     pluginDependencies = null;
172     if (doRead(element, context, mainDescriptor)) {
173       return false;
174     }
175
176     if (myVersion == null) {
177       myVersion = context.getDefaultVersion();
178     }
179
180     if (pluginDependencies != null) {
181       XmlReader.readDependencies(mainDescriptor, this, context, pathResolver, pluginDependencies);
182     }
183
184     return true;
185   }
186
187   @TestOnly
188   public void readForTest(@NotNull Element element) {
189     doRead(element, DescriptorListLoadingContext.createSingleDescriptorContext(Collections.emptySet()), this);
190   }
191
192   private boolean doRead(@NotNull Element element,
193                         @NotNull DescriptorListLoadingContext context,
194                         @NotNull IdeaPluginDescriptorImpl mainDescriptor) {
195     for (Content content : element.getContent()) {
196       if (!(content instanceof Element)) {
197         continue;
198       }
199
200       boolean clearContent = true;
201       Element child = (Element)content;
202       switch (child.getName()) {
203         case "extensions":
204           epNameToExtensionElements = XmlReader.readExtensions(this, epNameToExtensionElements, context, child);
205           break;
206
207         case "extensionPoints":
208           XmlReader.readExtensionPoints(mainDescriptor, this, child);
209           break;
210
211         case "actions":
212           if (myActionElements == null) {
213             myActionElements = new ArrayList<>(child.getChildren());
214           }
215           else {
216             myActionElements.addAll(child.getChildren());
217           }
218           clearContent = child.getAttributeValue("resource-bundle") == null;
219           break;
220
221         case "module":
222           String moduleName = child.getAttributeValue("value");
223           if (moduleName != null) {
224             if (myModules == null) {
225               myModules = Collections.singletonList(PluginId.getId(moduleName));
226             }
227             else {
228               if (myModules.size() == 1) {
229                 List<PluginId> singleton = myModules;
230                 myModules = new ArrayList<>(4);
231                 myModules.addAll(singleton);
232               }
233               myModules.add(PluginId.getId(moduleName));
234             }
235           }
236           break;
237
238         case "application-components":
239           // because of x-pointer, maybe several application-components tag in document
240           readComponents(child, appContainerDescriptor);
241           break;
242
243         case "project-components":
244           readComponents(child, projectContainerDescriptor);
245           break;
246
247         case "module-components":
248           readComponents(child, moduleContainerDescriptor);
249           break;
250
251         case "applicationListeners":
252           XmlReader.readListeners(child, appContainerDescriptor, mainDescriptor);
253           break;
254
255         case "projectListeners":
256           XmlReader.readListeners(child, projectContainerDescriptor, mainDescriptor);
257           break;
258
259         case "depends":
260           if (!readPluginDependency(basePath, context, child)) {
261             return true;
262           }
263           break;
264
265         case "incompatible-with":
266           readPluginIncompatibility(child);
267           break;
268
269         case "category":
270           myCategory = StringUtil.nullize(child.getTextTrim());
271           break;
272
273         case "change-notes":
274           myChangeNotes = StringUtil.nullize(child.getTextTrim());
275           break;
276
277         case "version":
278           myVersion = StringUtil.nullize(child.getTextTrim());
279           break;
280
281         case "description":
282           myDescriptionChildText = StringUtil.nullize(child.getTextTrim());
283           break;
284
285         case "resource-bundle":
286           String value = StringUtil.nullize(child.getTextTrim());
287           if (myResourceBundleBaseName != null && !Objects.equals(myResourceBundleBaseName, value)) {
288             context.getLogger().warn("Resource bundle redefinition for plugin '" + mainDescriptor.getPluginId() + "'. " +
289                      "Old value: " + myResourceBundleBaseName + ", new value: " + value);
290           }
291           myResourceBundleBaseName = value;
292           break;
293
294         case "product-descriptor":
295           readProduct(context, child);
296           break;
297
298         case "vendor":
299           myVendor = StringUtil.nullize(child.getTextTrim());
300           myVendorEmail = StringUtil.nullize(child.getAttributeValue("email"));
301           myVendorUrl = StringUtil.nullize(child.getAttributeValue("url"));
302           break;
303
304         case "idea-version":
305           mySinceBuild = StringUtil.nullize(child.getAttributeValue("since-build"));
306           myUntilBuild = StringUtil.nullize(child.getAttributeValue("until-build"));
307           if (!checkCompatibility(context)) {
308             return true;
309           }
310           break;
311       }
312
313       if (clearContent) {
314         child.getContent().clear();
315       }
316     }
317     return false;
318   }
319
320   private void readProduct(@NotNull DescriptorListLoadingContext context, @NotNull Element child) {
321     myProductCode = StringUtil.nullize(child.getAttributeValue("code"));
322     myReleaseDate = parseReleaseDate(child.getAttributeValue("release-date"), context);
323     myReleaseVersion = StringUtil.parseInt(child.getAttributeValue("release-version"), 0);
324     myIsLicenseOptional = Boolean.parseBoolean(child.getAttributeValue("optional", "false"));
325   }
326
327   private void readPluginIncompatibility(@NotNull Element child) {
328     String pluginId = child.getTextTrim();
329     if (pluginId.isEmpty()) return;
330
331     if (myIncompatibilities == null) {
332       myIncompatibilities = new ArrayList<>();
333     }
334     myIncompatibilities.add(PluginId.getId(pluginId));
335   }
336
337   private boolean readPluginDependency(@NotNull Path basePath, @NotNull DescriptorListLoadingContext context, @NotNull Element child) {
338     String dependencyIdString = child.getTextTrim();
339     if (dependencyIdString.isEmpty()) {
340       return true;
341     }
342
343     PluginId dependencyId = PluginId.getId(dependencyIdString);
344     boolean isOptional = Boolean.parseBoolean(child.getAttributeValue("optional"));
345     boolean isDisabledOrBroken = false;
346     // context.isPluginIncomplete must be not checked here as another version of plugin maybe supplied later from another source
347     if (context.isPluginDisabled(dependencyId)) {
348       if (!isOptional) {
349         markAsIncomplete(context, "Non-optional dependency plugin " + dependencyId + " is disabled", dependencyId);
350       }
351
352       isDisabledOrBroken = true;
353     }
354     else {
355       if (context.result.isBroken(dependencyId)) {
356         if (!isOptional) {
357           context.getLogger().info("Skipping reading of " + myId + " from " + basePath + " (reason: non-optional dependency " + dependencyId + " is broken)");
358           markAsIncomplete(context, "Non-optional dependency " + dependencyId + " is broken", null);
359           return false;
360         }
361
362         isDisabledOrBroken = true;
363       }
364     }
365
366     PluginDependency dependency = new PluginDependency(dependencyId, StringUtil.nullize(child.getAttributeValue("config-file")), isDisabledOrBroken);
367     dependency.isOptional = isOptional;
368     if (pluginDependencies == null) {
369       pluginDependencies = new ArrayList<>();
370     }
371     else {
372       // https://youtrack.jetbrains.com/issue/IDEA-206274
373       for (PluginDependency item : pluginDependencies) {
374         if (item.id == dependencyId) {
375           if (item.isOptional) {
376             if (!isOptional) {
377               item.isOptional = false;
378             }
379           }
380           else {
381             dependency.isOptional = false;
382             if (item.configFile == null) {
383               item.configFile = dependency.configFile;
384               return true;
385             }
386           }
387         }
388       }
389     }
390     pluginDependencies.add(dependency);
391     return true;
392   }
393
394   private boolean checkCompatibility(@NotNull DescriptorListLoadingContext context) {
395     String since = mySinceBuild;
396     String until = myUntilBuild;
397     if (isBundled() || (since == null && until == null)) {
398       return true;
399     }
400
401     String message = PluginManagerCore.getIncompatibleMessage(context.result.productBuildNumber.get(), since, until);
402     if (message == null) {
403       return true;
404     }
405
406     markAsIncomplete(context, null, null);  // error will be added by reportIncompatiblePlugin
407     context.result.reportIncompatiblePlugin(this, message, since, until);
408     return false;
409   }
410
411   @NotNull String formatErrorMessage(@NotNull String message) {
412     String path = this.path.toString();
413     StringBuilder builder = new StringBuilder();
414     builder.append("The ").append(myName).append(" (id=").append(myId).append(", path=");
415     builder.append(FileUtil.getLocationRelativeToUserHome(path, false));
416     if (myVersion != null && !isBundled() && !myVersion.equals(PluginManagerCore.getBuildNumber().asString())) {
417       builder.append(", version=").append(myVersion);
418     }
419     builder.append(") plugin ").append(message);
420     return builder.toString();
421   }
422
423   private void markAsIncomplete(@NotNull DescriptorListLoadingContext context, @Nullable String errorMessage, @Nullable PluginId disabledDependency) {
424     boolean wasIncomplete = incomplete;
425     incomplete = true;
426     setEnabled(false);
427     if (myId != null && !wasIncomplete) {
428       PluginError pluginError = errorMessage == null ? null : new PluginError(this, errorMessage, null, false);
429       if (pluginError != null && disabledDependency != null) {
430         pluginError.setDisabledDependency(disabledDependency);
431       }
432       context.result.addIncompletePlugin(this, pluginError);
433     }
434   }
435
436   private static void readComponents(@NotNull Element parent, @NotNull ContainerDescriptor containerDescriptor) {
437     List<Content> content = parent.getContent();
438     int contentSize = content.size();
439     if (contentSize == 0) {
440       return;
441     }
442
443     List<ComponentConfig> result = containerDescriptor.getComponentListToAdd(contentSize);
444     for (Content child : content) {
445       if (!(child instanceof Element)) {
446         continue;
447       }
448
449       Element componentElement = (Element)child;
450       if (!componentElement.getName().equals("component")) {
451         continue;
452       }
453
454       ComponentConfig componentConfig = new ComponentConfig();
455       Map<String, String> options = null;
456       loop:
457       for (Element elementChild : componentElement.getChildren()) {
458         switch (elementChild.getName()) {
459           case "skipForDefaultProject":
460             if (!readBoolValue(elementChild.getTextTrim())) {
461               componentConfig.setLoadForDefaultProject(true);
462             }
463             break;
464
465           case "loadForDefaultProject":
466             componentConfig.setLoadForDefaultProject(readBoolValue(elementChild.getTextTrim()));
467             break;
468
469           case "interface-class":
470             componentConfig.setInterfaceClass(elementChild.getTextTrim());
471             break;
472
473           case "implementation-class":
474             componentConfig.setImplementationClass(elementChild.getTextTrim());
475             break;
476
477           case "headless-implementation-class":
478             componentConfig.setHeadlessImplementationClass(elementChild.getTextTrim());
479             break;
480
481           case "option":
482             String name = elementChild.getAttributeValue("name");
483             String value = elementChild.getAttributeValue("value");
484             if (name != null) {
485               if (name.equals("os")) {
486                 if (value != null && !XmlReader.isSuitableForOs(value)) {
487                   continue loop;
488                 }
489               }
490               else {
491                 if (options == null) {
492                   options = Collections.singletonMap(name, value);
493                 }
494                 else {
495                   if (options.size() == 1) {
496                     options = new HashMap<>(options);
497                   }
498                   options.put(name, value);
499                 }
500               }
501             }
502             break;
503         }
504       }
505
506       if (options != null) {
507         componentConfig.options = options;
508       }
509
510       result.add(componentConfig);
511     }
512   }
513
514   private static boolean readBoolValue(@NotNull String value) {
515     return value.isEmpty() || value.equalsIgnoreCase("true");
516   }
517
518   private @Nullable Date parseReleaseDate(@Nullable String dateStr, @NotNull DescriptorListLoadingContext context) {
519     if (StringUtil.isEmpty(dateStr)) {
520       return null;
521     }
522
523     try {
524       return context.getDateParser().parse(dateStr);
525     }
526     catch (ParseException e) {
527       context.getLogger().info("Error parse release date from plugin descriptor for plugin " + myName + " {" + myId + "}: " + e.getMessage());
528     }
529     return null;
530   }
531
532   public static final Pattern EXPLICIT_BIG_NUMBER_PATTERN = Pattern.compile("(.*)\\.(9{4,}+|10{4,}+)");
533
534   /**
535    * Convert build number like '146.9999' to '146.*' (like plugin repository does) to ensure that plugins which have such values in
536    * 'until-build' attribute will be compatible with 146.SNAPSHOT build.
537    */
538   public static String convertExplicitBigNumberInUntilBuildToStar(@Nullable String build) {
539     if (build == null) return null;
540     Matcher matcher = EXPLICIT_BIG_NUMBER_PATTERN.matcher(build);
541     if (matcher.matches()) {
542       return matcher.group(1) + ".*";
543     }
544     return build;
545   }
546
547   public @NotNull ContainerDescriptor getAppContainerDescriptor() {
548     return appContainerDescriptor;
549   }
550
551   @ApiStatus.Internal
552   public void registerExtensions(@NotNull ExtensionsAreaImpl area,
553                                  @NotNull IdeaPluginDescriptorImpl rootDescriptor,
554                                  @NotNull ContainerDescriptor containerDescriptor,
555                                  @Nullable List<Runnable> listenerCallbacks) {
556     Map<String, List<Element>> extensions = containerDescriptor.extensions;
557     if (extensions != null) {
558       area.registerExtensions(extensions, rootDescriptor, listenerCallbacks);
559       return;
560     }
561
562     if (epNameToExtensionElements == null) {
563       return;
564     }
565
566     // app container: in most cases will be only app-level extensions - to reduce map copying, assume that all extensions are app-level and then filter out
567     // project container: rest of extensions wil be mostly project level
568     // module container: just use rest, area will not register unrelated extension anyway as no registered point
569     containerDescriptor.extensions = epNameToExtensionElements;
570
571     LinkedHashMap<String, List<Element>> other = null;
572     Iterator<Map.Entry<String, List<Element>>> iterator = containerDescriptor.extensions.entrySet().iterator();
573     while (iterator.hasNext()) {
574       Map.Entry<String, List<Element>> entry = iterator.next();
575       if (!area.registerExtensions(entry.getKey(), entry.getValue(), rootDescriptor, listenerCallbacks)) {
576         iterator.remove();
577         if (other == null) {
578           other = new LinkedHashMap<>();
579         }
580         addExtensionList(other, entry.getKey(), entry.getValue());
581       }
582     }
583     isExtensionsCleared = true;
584
585     if (containerDescriptor.extensions.isEmpty()) {
586       containerDescriptor.extensions = Collections.emptyMap();
587     }
588
589     if (containerDescriptor == projectContainerDescriptor) {
590       // assign unsorted to module level to avoid concurrent access during parallel module loading
591       moduleContainerDescriptor.extensions = other;
592       epNameToExtensionElements = null;
593     }
594     else {
595       epNameToExtensionElements = other;
596     }
597   }
598
599   @Override
600   public String getDescription() {
601     String result = myDescription;
602     if (result != null) {
603       return result;
604     }
605
606     ResourceBundle bundle = null;
607     if (myResourceBundleBaseName != null) {
608       try {
609         bundle = DynamicBundle.INSTANCE.getResourceBundle(myResourceBundleBaseName, getPluginClassLoader());
610       }
611       catch (MissingResourceException e) {
612         PluginManagerCore.getLogger().info("Cannot find plugin " + myId + " resource-bundle: " + myResourceBundleBaseName);
613       }
614     }
615
616     if (bundle == null) {
617       result = myDescriptionChildText;
618     }
619     else {
620       result = AbstractBundle.messageOrDefault(bundle, "plugin." + myId + ".description", StringUtil.notNullize(myDescriptionChildText));
621     }
622     myDescription = result;
623     return result;
624   }
625
626   @Override
627   public String getChangeNotes() {
628     return myChangeNotes;
629   }
630
631   @Override
632   public String getName() {
633     return myName;
634   }
635
636   @Override
637   public @Nullable String getProductCode() {
638     return myProductCode;
639   }
640
641   @Override
642   public @Nullable Date getReleaseDate() {
643     return myReleaseDate;
644   }
645
646   @Override
647   public int getReleaseVersion() {
648     return myReleaseVersion;
649   }
650
651   @Override
652   public boolean isLicenseOptional() {
653     return myIsLicenseOptional;
654   }
655
656   @Override
657   public @NotNull List<PluginId> getIncompatibleModuleIds() {
658     return ContainerUtil.notNullize(myIncompatibilities);
659   }
660
661   @SuppressWarnings("deprecation")
662   @Override
663   public PluginId @NotNull [] getDependentPluginIds() {
664     if (pluginDependencies == null || pluginDependencies.isEmpty()) {
665       return PluginId.EMPTY_ARRAY;
666     }
667     int size = pluginDependencies.size();
668     PluginId[] result = new PluginId[size];
669     for (int i = 0; i < size; i++) {
670       result[i] = pluginDependencies.get(i).id;
671     }
672     return result;
673   }
674
675   @Override
676   public PluginId @NotNull [] getOptionalDependentPluginIds() {
677     if (pluginDependencies == null || pluginDependencies.isEmpty()) {
678       return PluginId.EMPTY_ARRAY;
679     }
680     return pluginDependencies.stream().filter(it -> it.isOptional).map(it -> it.id).toArray(PluginId[]::new);
681   }
682
683   @Override
684   public String getVendor() {
685     return myVendor;
686   }
687
688   @Override
689   public String getVersion() {
690     return myVersion;
691   }
692
693   @Override
694   public String getResourceBundleBaseName() {
695     return myResourceBundleBaseName;
696   }
697
698   @Override
699   public String getCategory() {
700     return myCategory;
701   }
702
703   /*
704      This setter was explicitly defined to be able to set a category for a
705      descriptor outside its loading from the xml file.
706      Problem was that most commonly plugin authors do not publish the plugin's
707      category in its .xml file so to be consistent in plugins representation
708      (e.g. in the Plugins form) we have to set this value outside.
709   */
710   public void setCategory(String category) {
711     myCategory = category;
712   }
713
714   public @Nullable Map<String, List<Element>> getExtensions() {
715     if (isExtensionsCleared) {
716       throw new IllegalStateException("Trying to retrieve extensions list after extension elements have been cleared");
717     }
718     if (epNameToExtensionElements == null) {
719       return null;
720     }
721     else {
722       return new LinkedHashMap<>(epNameToExtensionElements);
723     }
724   }
725
726   /**
727    * @deprecated Do not use. If you want to get class loader for own plugin, just use your current class's class loader.
728    */
729   @Deprecated
730   public @NotNull List<File> getClassPath() {
731     File path = this.path.toFile();
732     if (!path.isDirectory()) {
733       return Collections.singletonList(path);
734     }
735
736     List<File> result = new ArrayList<>();
737     File classesDir = new File(path, "classes");
738     if (classesDir.exists()) {
739       result.add(classesDir);
740     }
741
742     File[] files = new File(path, "lib").listFiles();
743     if (files == null || files.length <= 0) {
744       return result;
745     }
746
747     for (File f : files) {
748       if (f.isFile()) {
749         String name = f.getName();
750         if (StringUtil.endsWithIgnoreCase(name, ".jar") || StringUtil.endsWithIgnoreCase(name, ".zip")) {
751           result.add(f);
752         }
753       }
754       else {
755         result.add(f);
756       }
757     }
758     return result;
759   }
760
761   @NotNull List<Path> collectClassPath(@NotNull Map<String, String[]> additionalLayoutMap) {
762     if (!Files.isDirectory(path)) {
763       return Collections.singletonList(path);
764     }
765
766     List<Path> result = new ArrayList<>();
767     Path classesDir = path.resolve("classes");
768     if (Files.exists(classesDir)) {
769       result.add(classesDir);
770     }
771
772     if (PluginManagerCore.usePluginClassLoader) {
773       Path productionDirectory = path.getParent();
774       if (productionDirectory.endsWith("production")) {
775         result.add(path);
776         String moduleName = path.getFileName().toString();
777         String[] additionalPaths = additionalLayoutMap.get(moduleName);
778         if (additionalPaths != null) {
779           for (String path : additionalPaths) {
780             result.add(productionDirectory.resolve(path));
781           }
782         }
783       }
784     }
785
786     try (DirectoryStream<Path> childStream = Files.newDirectoryStream(path.resolve("lib"))) {
787       for (Path f : childStream) {
788         if (Files.isRegularFile(f)) {
789           String name = f.getFileName().toString();
790           if (StringUtilRt.endsWithIgnoreCase(name, ".jar") || StringUtilRt.endsWithIgnoreCase(name, ".zip")) {
791             result.add(f);
792           }
793         }
794         else {
795           result.add(f);
796         }
797       }
798     }
799     catch (NoSuchFileException ignore) {
800     }
801     catch (IOException e) {
802       PluginManagerCore.getLogger().debug(e);
803     }
804     return result;
805   }
806
807   public @Nullable List<Element> getActionDescriptionElements() {
808     return myActionElements;
809   }
810
811   @Override
812   public String getVendorEmail() {
813     return myVendorEmail;
814   }
815
816   @Override
817   public String getVendorUrl() {
818     return myVendorUrl;
819   }
820
821   @Override
822   public String getUrl() {
823     return myUrl;
824   }
825
826   public void setUrl(String val) {
827     myUrl = val;
828   }
829
830   public boolean isDeleted() {
831     return myDeleted;
832   }
833
834   public void setDeleted(boolean deleted) {
835     myDeleted = deleted;
836   }
837
838   public void setLoader(@Nullable ClassLoader loader) {
839     myLoader = loader;
840   }
841
842   public boolean unloadClassLoader(int timeoutMs) {
843     GCWatcher watcher = GCWatcher.tracking(myLoader);
844     myLoader = null;
845     return watcher.tryCollect(timeoutMs);
846   }
847
848   @Override
849   public PluginId getPluginId() {
850     return myId;
851   }
852
853   @Override
854   public ClassLoader getPluginClassLoader() {
855     return myLoader != null ? myLoader : getClass().getClassLoader();
856   }
857
858   public boolean getUseIdeaClassLoader() {
859     return myUseIdeaClassLoader;
860   }
861
862   boolean isUseCoreClassLoader() {
863     return myUseCoreClassLoader;
864   }
865
866   void setUseCoreClassLoader() {
867     myUseCoreClassLoader = true;
868   }
869
870   @Override
871   public boolean isEnabled() {
872     return myEnabled;
873   }
874
875   @Override
876   public void setEnabled(final boolean enabled) {
877     myEnabled = enabled;
878   }
879
880   @Override
881   public String getSinceBuild() {
882     return mySinceBuild;
883   }
884
885   @Override
886   public String getUntilBuild() {
887     return myUntilBuild;
888   }
889
890   void mergeOptionalConfig(@NotNull IdeaPluginDescriptorImpl descriptor) {
891     if (epNameToExtensionElements == null) {
892       epNameToExtensionElements = descriptor.epNameToExtensionElements;
893     }
894     else if (descriptor.epNameToExtensionElements != null) {
895       descriptor.epNameToExtensionElements.forEach((name, list) -> {
896         addExtensionList(epNameToExtensionElements, name, list);
897       });
898     }
899
900     if (myActionElements == null) {
901       myActionElements = descriptor.myActionElements;
902     }
903     else if (descriptor.myActionElements != null) {
904       myActionElements.addAll(descriptor.myActionElements);
905     }
906
907     appContainerDescriptor.merge(descriptor.appContainerDescriptor);
908     projectContainerDescriptor.merge(descriptor.projectContainerDescriptor);
909     moduleContainerDescriptor.merge(descriptor.moduleContainerDescriptor);
910   }
911
912   private static void addExtensionList(@NotNull Map<String, List<Element>> map, @NotNull String name, @NotNull List<Element> list) {
913     List<Element> mapList = map.computeIfAbsent(name, __ -> list);
914     if (mapList != list) {
915       mapList.addAll(list);
916     }
917   }
918
919   @Override
920   public boolean isBundled() {
921     return myBundled;
922   }
923
924   @Override
925   public boolean allowBundledUpdate() {
926     return myAllowBundledUpdate;
927   }
928
929   @Override
930   public boolean isImplementationDetail() {
931     return myImplementationDetail;
932   }
933
934   @Override
935   public boolean isRequireRestart() {
936     return myRequireRestart;
937   }
938
939   public @NotNull List<PluginId> getModules() {
940     return myModules == null ? Collections.emptyList() : myModules;
941   }
942
943   @Override
944   public boolean equals(Object o) {
945     return this == o || o instanceof IdeaPluginDescriptorImpl && myId == ((IdeaPluginDescriptorImpl)o).myId;
946   }
947
948   @Override
949   public int hashCode() {
950     return Objects.hashCode(myId);
951   }
952
953   @Override
954   public String toString() {
955     return "PluginDescriptor(name=" + myName + ", id=" + myId + ", path=" + path + ")";
956   }
957 }