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