ddb17a834e20b50f17f9e0cba03ea8f9a2d5fa47
[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       if (myModules == null) {
327         myModules = Collections.singletonList(PluginId.getId(moduleName));
328       }
329       else {
330         if (myModules.size() == 1) {
331           List<PluginId> singleton = myModules;
332           myModules = new ArrayList<>(4);
333           myModules.addAll(singleton);
334         }
335         myModules.add(PluginId.getId(moduleName));
336       }
337     }
338   }
339
340   private void readProduct(@NotNull DescriptorListLoadingContext context, @NotNull Element child) {
341     myProductCode = StringUtil.nullize(child.getAttributeValue("code"));
342     myReleaseDate = parseReleaseDate(child.getAttributeValue("release-date"), context);
343     myReleaseVersion = StringUtil.parseInt(child.getAttributeValue("release-version"), 0);
344     myIsLicenseOptional = Boolean.parseBoolean(child.getAttributeValue("optional", "false"));
345   }
346
347   private void readPluginIncompatibility(@NotNull Element child) {
348     String pluginId = child.getTextTrim();
349     if (pluginId.isEmpty()) return;
350
351     if (incompatibilities == null) {
352       incompatibilities = new ArrayList<>();
353     }
354     incompatibilities.add(PluginId.getId(pluginId));
355   }
356
357   private boolean readPluginDependency(@NotNull Path basePath, @NotNull DescriptorListLoadingContext context, @NotNull Element child) {
358     String dependencyIdString = child.getTextTrim();
359     if (dependencyIdString.isEmpty()) {
360       return true;
361     }
362
363     PluginId dependencyId = PluginId.getId(dependencyIdString);
364     boolean isOptional = Boolean.parseBoolean(child.getAttributeValue("optional"));
365     boolean isDisabledOrBroken = false;
366     // context.isPluginIncomplete must be not checked here as another version of plugin maybe supplied later from another source
367     if (context.isPluginDisabled(dependencyId)) {
368       if (!isOptional) {
369         markAsIncomplete(context, () -> {
370           return CoreBundle.message("plugin.loading.error.short.depends.on.disabled.plugin", dependencyId);
371         }, dependencyId);
372       }
373
374       isDisabledOrBroken = true;
375     }
376     else {
377       if (context.result.isBroken(dependencyId)) {
378         if (!isOptional) {
379           DescriptorListLoadingContext.LOG.info("Skipping reading of " + myId + " from " + basePath + " (reason: non-optional dependency " + dependencyId + " is broken)");
380           markAsIncomplete(context, CoreBundle.messagePointer("plugin.loading.error.short.depends.on.broken.plugin", dependencyId), null);
381           return false;
382         }
383
384         isDisabledOrBroken = true;
385       }
386     }
387
388     PluginDependency dependency = new PluginDependency(dependencyId, StringUtil.nullize(child.getAttributeValue("config-file")), isDisabledOrBroken);
389     dependency.isOptional = isOptional;
390     if (pluginDependencies == null) {
391       pluginDependencies = new ArrayList<>();
392     }
393     else {
394       // https://youtrack.jetbrains.com/issue/IDEA-206274
395       for (PluginDependency item : pluginDependencies) {
396         if (item.id == dependencyId) {
397           if (item.isOptional) {
398             if (!isOptional) {
399               item.isOptional = false;
400             }
401           }
402           else {
403             dependency.isOptional = false;
404             if (item.configFile == null) {
405               item.configFile = dependency.configFile;
406               return true;
407             }
408           }
409         }
410       }
411     }
412     pluginDependencies.add(dependency);
413     return true;
414   }
415
416   private boolean checkCompatibility(@NotNull DescriptorListLoadingContext context) {
417     String since = mySinceBuild;
418     String until = myUntilBuild;
419     if (isBundled() || (since == null && until == null)) {
420       return true;
421     }
422
423     @Nullable PluginLoadingError error = PluginManagerCore.checkBuildNumberCompatibility(this, context.result.productBuildNumber.get());
424     if (error == null) {
425       return true;
426     }
427
428     markAsIncomplete(context, null, null);  // error will be added by reportIncompatiblePlugin
429     context.result.reportIncompatiblePlugin(this, error);
430     return false;
431   }
432
433   private void markAsIncomplete(@NotNull DescriptorListLoadingContext context, @Nullable Supplier<@Nls String> shortMessage, @Nullable PluginId disabledDependency) {
434     boolean wasIncomplete = incomplete;
435     incomplete = true;
436     setEnabled(false);
437     if (myId != null && !wasIncomplete) {
438       PluginLoadingError pluginError = shortMessage == null ? null : PluginLoadingError.createWithoutNotification(this, shortMessage);
439       if (pluginError != null && disabledDependency != null) {
440         pluginError.setDisabledDependency(disabledDependency);
441       }
442       context.result.addIncompletePlugin(this, pluginError);
443     }
444   }
445
446   private static void readComponents(@NotNull Element parent, @NotNull ContainerDescriptor containerDescriptor) {
447     List<Content> content = parent.getContent();
448     int contentSize = content.size();
449     if (contentSize == 0) {
450       return;
451     }
452
453     List<ComponentConfig> result = containerDescriptor.getComponentListToAdd(contentSize);
454     for (Content child : content) {
455       if (!(child instanceof Element)) {
456         continue;
457       }
458
459       Element componentElement = (Element)child;
460       if (!componentElement.getName().equals("component")) {
461         continue;
462       }
463
464       ComponentConfig componentConfig = new ComponentConfig();
465       Map<String, String> options = null;
466       loop:
467       for (Element elementChild : componentElement.getChildren()) {
468         switch (elementChild.getName()) {
469           case "skipForDefaultProject":
470             if (!readBoolValue(elementChild.getTextTrim())) {
471               componentConfig.setLoadForDefaultProject(true);
472             }
473             break;
474
475           case "loadForDefaultProject":
476             componentConfig.setLoadForDefaultProject(readBoolValue(elementChild.getTextTrim()));
477             break;
478
479           case "interface-class":
480             componentConfig.setInterfaceClass(elementChild.getTextTrim());
481             break;
482
483           case "implementation-class":
484             componentConfig.setImplementationClass(elementChild.getTextTrim());
485             break;
486
487           case "headless-implementation-class":
488             componentConfig.setHeadlessImplementationClass(elementChild.getTextTrim());
489             break;
490
491           case "option":
492             String name = elementChild.getAttributeValue("name");
493             String value = elementChild.getAttributeValue("value");
494             if (name != null) {
495               if (name.equals("os")) {
496                 if (value != null && !XmlReader.isSuitableForOs(value)) {
497                   continue loop;
498                 }
499               }
500               else {
501                 if (options == null) {
502                   options = Collections.singletonMap(name, value);
503                 }
504                 else {
505                   if (options.size() == 1) {
506                     options = new HashMap<>(options);
507                   }
508                   options.put(name, value);
509                 }
510               }
511             }
512             break;
513         }
514       }
515
516       if (options != null) {
517         componentConfig.options = options;
518       }
519
520       result.add(componentConfig);
521     }
522   }
523
524   private static boolean readBoolValue(@NotNull String value) {
525     return value.isEmpty() || value.equalsIgnoreCase("true");
526   }
527
528   private @Nullable Date parseReleaseDate(@Nullable String dateStr, @NotNull DescriptorListLoadingContext context) {
529     if (StringUtil.isEmpty(dateStr)) {
530       return null;
531     }
532
533     try {
534       return context.getDateParser().parse(dateStr);
535     }
536     catch (ParseException e) {
537       DescriptorListLoadingContext.LOG.info("Error parse release date from plugin descriptor for plugin " + myName + " {" + myId + "}: " + e.getMessage());
538     }
539     return null;
540   }
541
542   public static final Pattern EXPLICIT_BIG_NUMBER_PATTERN = Pattern.compile("(.*)\\.(9{4,}+|10{4,}+)");
543
544   /**
545    * Convert build number like '146.9999' to '146.*' (like plugin repository does) to ensure that plugins which have such values in
546    * 'until-build' attribute will be compatible with 146.SNAPSHOT build.
547    */
548   public static String convertExplicitBigNumberInUntilBuildToStar(@Nullable String build) {
549     if (build == null) return null;
550     Matcher matcher = EXPLICIT_BIG_NUMBER_PATTERN.matcher(build);
551     if (matcher.matches()) {
552       return matcher.group(1) + ".*";
553     }
554     return build;
555   }
556
557   public @NotNull ContainerDescriptor getAppContainerDescriptor() {
558     return appContainerDescriptor;
559   }
560
561   @ApiStatus.Internal
562   public void registerExtensions(@NotNull ExtensionsAreaImpl area,
563                                  @NotNull IdeaPluginDescriptorImpl rootDescriptor,
564                                  @NotNull ContainerDescriptor containerDescriptor,
565                                  @Nullable List<? super Runnable> listenerCallbacks) {
566     Map<String, List<Element>> extensions = containerDescriptor.extensions;
567     if (extensions != null) {
568       area.registerExtensions(extensions, rootDescriptor, listenerCallbacks);
569       return;
570     }
571
572     if (epNameToExtensionElements == null) {
573       return;
574     }
575
576     // 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
577     // project container: rest of extensions wil be mostly project level
578     // module container: just use rest, area will not register unrelated extension anyway as no registered point
579     containerDescriptor.extensions = epNameToExtensionElements;
580
581     LinkedHashMap<String, List<Element>> other = null;
582     Iterator<Map.Entry<String, List<Element>>> iterator = containerDescriptor.extensions.entrySet().iterator();
583     while (iterator.hasNext()) {
584       Map.Entry<String, List<Element>> entry = iterator.next();
585       if (!area.registerExtensions(entry.getKey(), entry.getValue(), rootDescriptor, listenerCallbacks)) {
586         iterator.remove();
587         if (other == null) {
588           other = new LinkedHashMap<>();
589         }
590         addExtensionList(other, entry.getKey(), entry.getValue());
591       }
592     }
593     isExtensionsCleared = true;
594
595     if (containerDescriptor.extensions.isEmpty()) {
596       containerDescriptor.extensions = Collections.emptyMap();
597     }
598
599     if (containerDescriptor == projectContainerDescriptor) {
600       // assign unsorted to module level to avoid concurrent access during parallel module loading
601       moduleContainerDescriptor.extensions = other;
602       epNameToExtensionElements = null;
603     }
604     else {
605       epNameToExtensionElements = other;
606     }
607   }
608
609   @Override
610   public String getDescription() {
611     @NlsSafe String result = myDescription;
612     if (result != null) {
613       return result;
614     }
615
616     ResourceBundle bundle = null;
617     if (myResourceBundleBaseName != null) {
618       try {
619         bundle = DynamicBundle.INSTANCE.getResourceBundle(myResourceBundleBaseName, getPluginClassLoader());
620       }
621       catch (MissingResourceException e) {
622         PluginManagerCore.getLogger().info("Cannot find plugin " + myId + " resource-bundle: " + myResourceBundleBaseName);
623       }
624     }
625
626     if (bundle == null) {
627       result = myDescriptionChildText;
628     }
629     else {
630       result = AbstractBundle.messageOrDefault(bundle, "plugin." + myId + ".description", StringUtil.notNullize(myDescriptionChildText));
631     }
632     myDescription = result;
633     return result;
634   }
635
636   @Override
637   public String getChangeNotes() {
638     return myChangeNotes;
639   }
640
641   @Override
642   public String getName() {
643     return myName;
644   }
645
646   @Override
647   public @Nullable String getProductCode() {
648     return myProductCode;
649   }
650
651   @Override
652   public @Nullable Date getReleaseDate() {
653     return myReleaseDate;
654   }
655
656   @Override
657   public int getReleaseVersion() {
658     return myReleaseVersion;
659   }
660
661   @Override
662   public boolean isLicenseOptional() {
663     return myIsLicenseOptional;
664   }
665
666   @SuppressWarnings("deprecation")
667   @Override
668   public PluginId @NotNull [] getDependentPluginIds() {
669     if (pluginDependencies == null || pluginDependencies.isEmpty()) {
670       return PluginId.EMPTY_ARRAY;
671     }
672     int size = pluginDependencies.size();
673     PluginId[] result = new PluginId[size];
674     for (int i = 0; i < size; i++) {
675       result[i] = pluginDependencies.get(i).id;
676     }
677     return result;
678   }
679
680   @Override
681   public PluginId @NotNull [] getOptionalDependentPluginIds() {
682     if (pluginDependencies == null || pluginDependencies.isEmpty()) {
683       return PluginId.EMPTY_ARRAY;
684     }
685     return pluginDependencies.stream().filter(it -> it.isOptional).map(it -> it.id).toArray(PluginId[]::new);
686   }
687
688   @Override
689   public String getVendor() {
690     return myVendor;
691   }
692
693   @Override
694   public String getVersion() {
695     return myVersion;
696   }
697
698   @Override
699   public String getResourceBundleBaseName() {
700     return myResourceBundleBaseName;
701   }
702
703   @Override
704   public String getCategory() {
705     return myCategory;
706   }
707
708   /*
709      This setter was explicitly defined to be able to set a category for a
710      descriptor outside its loading from the xml file.
711      Problem was that most commonly plugin authors do not publish the plugin's
712      category in its .xml file so to be consistent in plugins representation
713      (e.g. in the Plugins form) we have to set this value outside.
714   */
715   public void setCategory(String category) {
716     myCategory = category;
717   }
718
719   public @Nullable Map<String, List<Element>> getExtensions() {
720     if (isExtensionsCleared) {
721       throw new IllegalStateException("Trying to retrieve extensions list after extension elements have been cleared");
722     }
723     if (epNameToExtensionElements == null) {
724       return null;
725     }
726     else {
727       return new LinkedHashMap<>(epNameToExtensionElements);
728     }
729   }
730
731   /**
732    * @deprecated Do not use. If you want to get class loader for own plugin, just use your current class's class loader.
733    */
734   @Deprecated
735   public @NotNull List<File> getClassPath() {
736     File path = this.path.toFile();
737     if (!path.isDirectory()) {
738       return Collections.singletonList(path);
739     }
740
741     List<File> result = new ArrayList<>();
742     File classesDir = new File(path, "classes");
743     if (classesDir.exists()) {
744       result.add(classesDir);
745     }
746
747     File[] files = new File(path, "lib").listFiles();
748     if (files == null || files.length <= 0) {
749       return result;
750     }
751
752     for (File f : files) {
753       if (f.isFile()) {
754         String name = f.getName();
755         if (StringUtil.endsWithIgnoreCase(name, ".jar") || StringUtil.endsWithIgnoreCase(name, ".zip")) {
756           result.add(f);
757         }
758       }
759       else {
760         result.add(f);
761       }
762     }
763     return result;
764   }
765
766   @NotNull List<Path> collectClassPath(@NotNull Map<String, String[]> additionalLayoutMap) {
767     if (!Files.isDirectory(path)) {
768       return Collections.singletonList(path);
769     }
770
771     List<Path> result = new ArrayList<>();
772     Path classesDir = path.resolve("classes");
773     if (Files.exists(classesDir)) {
774       result.add(classesDir);
775     }
776
777     if (PluginManagerCore.usePluginClassLoader) {
778       Path productionDirectory = path.getParent();
779       if (productionDirectory.endsWith("production")) {
780         result.add(path);
781         String moduleName = path.getFileName().toString();
782         String[] additionalPaths = additionalLayoutMap.get(moduleName);
783         if (additionalPaths != null) {
784           for (String path : additionalPaths) {
785             result.add(productionDirectory.resolve(path));
786           }
787         }
788       }
789     }
790
791     try (DirectoryStream<Path> childStream = Files.newDirectoryStream(path.resolve("lib"))) {
792       for (Path f : childStream) {
793         if (Files.isRegularFile(f)) {
794           String name = f.getFileName().toString();
795           if (StringUtilRt.endsWithIgnoreCase(name, ".jar") || StringUtilRt.endsWithIgnoreCase(name, ".zip")) {
796             result.add(f);
797           }
798         }
799         else {
800           result.add(f);
801         }
802       }
803     }
804     catch (NoSuchFileException ignore) {
805     }
806     catch (IOException e) {
807       PluginManagerCore.getLogger().debug(e);
808     }
809     return result;
810   }
811
812   public @Nullable List<Element> getActionDescriptionElements() {
813     return myActionElements;
814   }
815
816   @Override
817   public String getVendorEmail() {
818     return myVendorEmail;
819   }
820
821   @Override
822   public String getVendorUrl() {
823     return myVendorUrl;
824   }
825
826   @Override
827   public String getUrl() {
828     return myUrl;
829   }
830
831   public void setUrl(String val) {
832     myUrl = val;
833   }
834
835   public boolean isDeleted() {
836     return myDeleted;
837   }
838
839   public void setDeleted(boolean deleted) {
840     myDeleted = deleted;
841   }
842
843   public void setLoader(@Nullable ClassLoader loader) {
844     myLoader = loader;
845   }
846
847   public boolean unloadClassLoader(int timeoutMs) {
848     if (timeoutMs == 0) {
849       myLoader = null;
850       return true;
851     }
852
853     GCWatcher watcher = GCWatcher.tracking(myLoader);
854     myLoader = null;
855     return watcher.tryCollect(timeoutMs);
856   }
857
858   @Override
859   public PluginId getPluginId() {
860     return myId;
861   }
862
863   @Override
864   public ClassLoader getPluginClassLoader() {
865     return myLoader != null ? myLoader : getClass().getClassLoader();
866   }
867
868   public boolean isUseIdeaClassLoader() {
869     return myUseIdeaClassLoader;
870   }
871
872   boolean isUseCoreClassLoader() {
873     return myUseCoreClassLoader;
874   }
875
876   void setUseCoreClassLoader() {
877     myUseCoreClassLoader = true;
878   }
879
880   @Override
881   public boolean isEnabled() {
882     return myEnabled;
883   }
884
885   @Override
886   public void setEnabled(final boolean enabled) {
887     myEnabled = enabled;
888   }
889
890   public boolean isExtensionsCleared() {
891     return isExtensionsCleared;
892   }
893
894   @Override
895   public String getSinceBuild() {
896     return mySinceBuild;
897   }
898
899   @Override
900   public String getUntilBuild() {
901     return myUntilBuild;
902   }
903
904   void mergeOptionalConfig(@NotNull IdeaPluginDescriptorImpl descriptor) {
905     if (epNameToExtensionElements == null) {
906       epNameToExtensionElements = descriptor.epNameToExtensionElements;
907     }
908     else if (descriptor.epNameToExtensionElements != null) {
909       descriptor.epNameToExtensionElements.forEach((name, list) -> addExtensionList(epNameToExtensionElements, name, list));
910     }
911
912     if (myActionElements == null) {
913       myActionElements = descriptor.myActionElements;
914     }
915     else if (descriptor.myActionElements != null) {
916       myActionElements.addAll(descriptor.myActionElements);
917     }
918
919     appContainerDescriptor.merge(descriptor.appContainerDescriptor);
920     projectContainerDescriptor.merge(descriptor.projectContainerDescriptor);
921     moduleContainerDescriptor.merge(descriptor.moduleContainerDescriptor);
922   }
923
924   private static void addExtensionList(@NotNull Map<String, List<Element>> map, @NotNull String name, @NotNull List<Element> list) {
925     List<Element> mapList = map.computeIfAbsent(name, __ -> list);
926     if (mapList != list) {
927       mapList.addAll(list);
928     }
929   }
930
931   @Override
932   public boolean isBundled() {
933     return myBundled;
934   }
935
936   @Override
937   public boolean allowBundledUpdate() {
938     return myAllowBundledUpdate;
939   }
940
941   @Override
942   public boolean isImplementationDetail() {
943     return myImplementationDetail;
944   }
945
946   @Override
947   public boolean isRequireRestart() {
948     return myRequireRestart;
949   }
950
951   public @NotNull List<PluginId> getModules() {
952     return myModules == null ? Collections.emptyList() : myModules;
953   }
954
955   @Override
956   public boolean equals(Object o) {
957     return this == o || o instanceof IdeaPluginDescriptorImpl && myId == ((IdeaPluginDescriptorImpl)o).myId;
958   }
959
960   @Override
961   public int hashCode() {
962     return Objects.hashCode(myId);
963   }
964
965   @Override
966   public String toString() {
967     return "PluginDescriptor(name=" + myName + ", id=" + myId + ", path=" + path + ", version=" + myVersion + ")";
968   }
969 }