e74df8b156097c3281f1c0c4e652ed97ed19e44f
[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.diagnostic.PluginException;
7 import com.intellij.openapi.Disposable;
8 import com.intellij.openapi.components.ComponentConfig;
9 import com.intellij.openapi.components.ComponentManager;
10 import com.intellij.openapi.components.ServiceDescriptor;
11 import com.intellij.openapi.extensions.PluginDescriptor;
12 import com.intellij.openapi.extensions.PluginId;
13 import com.intellij.openapi.extensions.impl.BeanExtensionPoint;
14 import com.intellij.openapi.extensions.impl.ExtensionPointImpl;
15 import com.intellij.openapi.extensions.impl.ExtensionsAreaImpl;
16 import com.intellij.openapi.extensions.impl.InterfaceExtensionPoint;
17 import com.intellij.openapi.util.Comparing;
18 import com.intellij.openapi.util.JDOMUtil;
19 import com.intellij.openapi.util.SafeJdomFactory;
20 import com.intellij.openapi.util.SystemInfo;
21 import com.intellij.openapi.util.io.FileUtil;
22 import com.intellij.openapi.util.text.StringUtil;
23 import com.intellij.openapi.util.text.StringUtilRt;
24 import com.intellij.util.SmartList;
25 import com.intellij.util.containers.ContainerUtil;
26 import com.intellij.util.containers.ContainerUtilRt;
27 import com.intellij.util.containers.Interner;
28 import com.intellij.util.messages.ListenerDescriptor;
29 import com.intellij.util.ref.GCWatcher;
30 import gnu.trove.THashMap;
31 import org.jdom.Attribute;
32 import org.jdom.Content;
33 import org.jdom.Element;
34 import org.jdom.JDOMException;
35 import org.jetbrains.annotations.ApiStatus;
36 import org.jetbrains.annotations.NotNull;
37 import org.jetbrains.annotations.Nullable;
38
39 import java.io.File;
40 import java.io.IOException;
41 import java.nio.file.DirectoryStream;
42 import java.nio.file.Files;
43 import java.nio.file.NoSuchFileException;
44 import java.nio.file.Path;
45 import java.text.ParseException;
46 import java.util.*;
47 import java.util.regex.Matcher;
48 import java.util.regex.Pattern;
49
50 public final class IdeaPluginDescriptorImpl implements IdeaPluginDescriptor, PluginDescriptor {
51   public enum OS {
52     mac, linux, windows, unix, freebsd
53   }
54
55   public static final IdeaPluginDescriptorImpl[] EMPTY_ARRAY = new IdeaPluginDescriptorImpl[0];
56
57   static final String APPLICATION_SERVICE = "com.intellij.applicationService";
58   static final String PROJECT_SERVICE = "com.intellij.projectService";
59   static final String MODULE_SERVICE = "com.intellij.moduleService";
60
61   private final Path myPath;
62   private Path myBasePath;   // base path for resolving optional dependency descriptors
63   private final boolean myBundled;
64   private String myName;
65   private PluginId myId;
66   private volatile String myDescription;
67   private @Nullable String myProductCode;
68   private @Nullable Date myReleaseDate;
69   private int myReleaseVersion;
70   private boolean myIsLicenseOptional;
71   private String myResourceBundleBaseName;
72   private String myChangeNotes;
73   private String myVersion;
74   private String myVendor;
75   private String myVendorEmail;
76   private String myVendorUrl;
77   private String myCategory;
78   private String myUrl;
79   private PluginId[] myDependencies = PluginId.EMPTY_ARRAY;
80   private PluginId[] myOptionalDependencies = PluginId.EMPTY_ARRAY;
81   private List<PluginId> myIncompatibilities;
82   private List<PluginDependency> myPluginDependencies;
83
84   // used only during initializing
85   transient Map<PluginId, List<IdeaPluginDescriptorImpl>> optionalConfigs;
86   transient List<Path> jarFiles;
87
88   private @Nullable List<Element> myActionElements;
89   // extension point name -> list of extension elements
90   private @Nullable THashMap<String, List<Element>> myExtensions;
91
92   private final ContainerDescriptor myAppContainerDescriptor = new ContainerDescriptor();
93   private final ContainerDescriptor myProjectContainerDescriptor = new ContainerDescriptor();
94   private final ContainerDescriptor myModuleContainerDescriptor = new ContainerDescriptor();
95
96   private List<PluginId> myModules;
97   private ClassLoader myLoader;
98   private String myDescriptionChildText;
99   private boolean myUseIdeaClassLoader;
100   private boolean myUseCoreClassLoader;
101   private boolean myAllowBundledUpdate;
102   private boolean myImplementationDetail;
103   private String mySinceBuild;
104   private String myUntilBuild;
105
106   private boolean myEnabled = true;
107   private boolean myDeleted;
108   private boolean myExtensionsCleared = false;
109
110   boolean incomplete;
111
112   public IdeaPluginDescriptorImpl(@NotNull Path pluginPath, boolean bundled) {
113     myPath = pluginPath;
114     myBundled = bundled;
115   }
116
117   @NotNull
118   @ApiStatus.Internal
119   public ContainerDescriptor getApp() {
120     return myAppContainerDescriptor;
121   }
122
123   @NotNull
124   @ApiStatus.Internal
125   public ContainerDescriptor getProject() {
126     return myProjectContainerDescriptor;
127   }
128
129   @NotNull
130   @ApiStatus.Internal
131   public ContainerDescriptor getModule() {
132     return myModuleContainerDescriptor;
133   }
134
135   @Override
136   public File getPath() {
137     return myPath.toFile();
138   }
139
140   @NotNull
141   @Override
142   public Path getPluginPath() {
143     return myPath;
144   }
145
146   public Path getBasePath() {
147     return myBasePath;
148   }
149
150   /**
151    * @deprecated Use {@link PluginManager#loadDescriptorFromFile(IdeaPluginDescriptorImpl, Path, SafeJdomFactory, boolean, Set)}
152    */
153   @Deprecated
154   public void loadFromFile(@NotNull File file, @Nullable SafeJdomFactory factory, boolean ignoreMissingInclude)
155     throws IOException, JDOMException {
156     PluginManager.loadDescriptorFromFile(this, file.toPath(), factory, ignoreMissingInclude, PluginManagerCore.disabledPlugins());
157   }
158
159   public boolean readExternal(@NotNull Element element,
160                               @NotNull Path basePath,
161                               @NotNull PathBasedJdomXIncluder.PathResolver<?> pathResolver,
162                               @NotNull DescriptorLoadingContext context,
163                               @NotNull IdeaPluginDescriptorImpl rootDescriptor) {
164     myBasePath = basePath;
165
166     // root element always `!isIncludeElement`, and it means that result always is a singleton list
167     // (also, plugin xml describes one plugin, this descriptor is not able to represent several plugins)
168     if (JDOMUtil.isEmpty(element)) {
169       markAsIncomplete(context);
170       return false;
171     }
172
173     XmlReader.readIdAndName(this, element);
174
175     if (myId != null && context.isPluginDisabled(myId)) {
176       markAsIncomplete(context);
177     }
178     else {
179       PathBasedJdomXIncluder.resolveNonXIncludeElement(element, basePath, context, pathResolver);
180       if (myId == null || myName == null) {
181         // read again after resolve
182         XmlReader.readIdAndName(this, element);
183
184         if (myId != null && context.isPluginDisabled(myId)) {
185           markAsIncomplete(context);
186         }
187       }
188     }
189
190     if (incomplete) {
191       myDescriptionChildText = element.getChildTextTrim("description");
192       myCategory = element.getChildTextTrim("category");
193       myVersion = element.getChildTextTrim("version");
194       if (context.parentContext.getLogger().isDebugEnabled()) {
195         context.parentContext.getLogger().debug("Skipping reading of " + myId + " from " + basePath + " (reason: disabled)");
196       }
197       List<Element> dependsElements = element.getChildren("depends");
198       for (Element dependsElement : dependsElements) {
199         readPluginDependency(basePath, context, dependsElement);
200       }
201       if (myPluginDependencies != null) {
202         int size = XmlReader.collapseDuplicateDependencies(myPluginDependencies);
203         XmlReader.collectDependentPluginIds(this, size);
204       }
205       return false;
206     }
207
208     XmlReader.readMetaInfo(this, element);
209
210     myPluginDependencies = null;
211     for (Content content : element.getContent()) {
212       if (!(content instanceof Element)) {
213         continue;
214       }
215
216       boolean clearContent = true;
217       Element child = (Element)content;
218       switch (child.getName()) {
219         case "extensions":
220           XmlReader.readExtensions(this, context.parentContext, child);
221           break;
222
223         case "extensionPoints":
224           XmlReader.readExtensionPoints(rootDescriptor, this, child);
225           break;
226
227         case "actions":
228           if (myActionElements == null) {
229             myActionElements = new ArrayList<>(child.getChildren());
230           }
231           else {
232             myActionElements.addAll(child.getChildren());
233           }
234           clearContent = child.getAttributeValue("resource-bundle") == null;
235           break;
236
237         case "module":
238           String moduleName = child.getAttributeValue("value");
239           if (moduleName != null) {
240             if (myModules == null) {
241               myModules = Collections.singletonList(PluginId.getId(moduleName));
242             }
243             else {
244               if (myModules.size() == 1) {
245                 List<PluginId> singleton = myModules;
246                 myModules = new ArrayList<>(4);
247                 myModules.addAll(singleton);
248               }
249               myModules.add(PluginId.getId(moduleName));
250             }
251           }
252           break;
253
254         case "application-components":
255           // because of x-pointer, maybe several application-components tag in document
256           readComponents(child, myAppContainerDescriptor);
257           break;
258
259         case "project-components":
260           readComponents(child, myProjectContainerDescriptor);
261           break;
262
263         case "module-components":
264           readComponents(child, myModuleContainerDescriptor);
265           break;
266
267         case "applicationListeners":
268           XmlReader.readListeners(this, child, myAppContainerDescriptor);
269           break;
270
271         case "projectListeners":
272           XmlReader.readListeners(this, child, myProjectContainerDescriptor);
273           break;
274
275         case "depends":
276           if (!readPluginDependency(basePath, context, child)) return false;
277           break;
278
279         case "incompatible-with":
280           readPluginIncompatibility(child);
281           break;
282
283         case "category":
284           myCategory = StringUtil.nullize(child.getTextTrim());
285           break;
286
287         case "change-notes":
288           myChangeNotes = StringUtil.nullize(child.getTextTrim());
289           break;
290
291         case "version":
292           myVersion = StringUtil.nullize(child.getTextTrim());
293           break;
294
295         case "description":
296           myDescriptionChildText = StringUtil.nullize(child.getTextTrim());
297           break;
298
299         case "resource-bundle":
300           String value = StringUtil.nullize(child.getTextTrim());
301           if (myResourceBundleBaseName != null && !Comparing.equal(myResourceBundleBaseName, value)) {
302             context.parentContext.getLogger().warn("Resource bundle redefinition for plugin '" + rootDescriptor.getPluginId() + "'. " +
303                      "Old value: " + myResourceBundleBaseName + ", new value: " + value);
304           }
305           myResourceBundleBaseName = value;
306           break;
307
308         case "product-descriptor":
309           myProductCode = StringUtil.nullize(child.getAttributeValue("code"));
310           myReleaseDate = parseReleaseDate(child.getAttributeValue("release-date"), context.parentContext);
311           myReleaseVersion = StringUtil.parseInt(child.getAttributeValue("release-version"), 0);
312           myIsLicenseOptional = Boolean.parseBoolean(child.getAttributeValue("optional", "false"));
313           break;
314
315         case "vendor":
316           myVendor = StringUtil.nullize(child.getTextTrim());
317           myVendorEmail = StringUtil.nullize(child.getAttributeValue("email"));
318           myVendorUrl = StringUtil.nullize(child.getAttributeValue("url"));
319           break;
320
321         case "idea-version":
322           mySinceBuild = StringUtil.nullize(child.getAttributeValue("since-build"));
323           myUntilBuild = StringUtil.nullize(child.getAttributeValue("until-build"));
324           if (!checkCompatibility(context)) {
325             return false;
326           }
327           break;
328       }
329
330       if (clearContent) {
331         child.getContent().clear();
332       }
333     }
334
335     if (myVersion == null) {
336       myVersion = context.parentContext.getDefaultVersion();
337     }
338
339     if (myPluginDependencies != null) {
340       XmlReader.readDependencies(rootDescriptor, this, context, pathResolver);
341     }
342
343     return true;
344   }
345
346   private void readPluginIncompatibility(@NotNull Element child) {
347     String pluginId = child.getTextTrim();
348     if (pluginId.isEmpty()) return;
349
350     if (myIncompatibilities == null) {
351       myIncompatibilities = new ArrayList<>();
352     }
353     myIncompatibilities.add(PluginId.getId(pluginId));
354   }
355
356   private boolean readPluginDependency(@NotNull Path basePath, @NotNull DescriptorLoadingContext context, Element child) {
357     String dependencyIdString = child.getTextTrim();
358     if (dependencyIdString.isEmpty()) {
359       return true;
360
361     }
362     PluginId dependencyId = PluginId.getId(dependencyIdString);
363     boolean isOptional = Boolean.parseBoolean(child.getAttributeValue("optional"));
364     boolean isAvailable = true;
365     IdeaPluginDescriptorImpl dependencyDescriptor = null;
366     if (context.isPluginDisabled(dependencyId) || context.isPluginIncomplete(dependencyId)) {
367       if (!isOptional) {
368         markAsIncomplete(context);
369       }
370
371       isAvailable = false;
372     }
373     else {
374       dependencyDescriptor = context.parentContext.result.idMap.get(dependencyId);
375       if (dependencyDescriptor != null && context.isBroken(dependencyDescriptor)) {
376         if (!isOptional) {
377           context.parentContext.getLogger().info("Skipping reading of " + myId + " from " + basePath + " (reason: non-optional dependency " + dependencyId + " is broken)");
378           markAsIncomplete(context);
379           return false;
380         }
381
382         isAvailable = false;
383       }
384     }
385
386     PluginDependency dependency = new PluginDependency();
387     dependency.pluginId = dependencyId;
388     dependency.optional = isOptional;
389     dependency.available = isAvailable;
390     dependency.configFile = StringUtil.nullize(child.getAttributeValue("config-file"));
391     dependency.dependency = dependencyDescriptor;
392     if (myPluginDependencies == null) {
393       myPluginDependencies = new ArrayList<>();
394     }
395     myPluginDependencies.add(dependency);
396     return true;
397   }
398
399   private boolean checkCompatibility(@NotNull DescriptorLoadingContext context) {
400     String since = mySinceBuild;
401     String until = myUntilBuild;
402     if (isBundled() || (since == null && until == null)) {
403       return true;
404     }
405
406     String message = PluginManagerCore.isIncompatible(context.parentContext.result.productBuildNumber, since, until);
407     if (message == null) {
408       return true;
409     }
410
411     markAsIncomplete(context);
412     context.parentContext.result.reportIncompatiblePlugin(this, message, since, until);
413     return false;
414   }
415
416   @NotNull
417   String formatErrorMessage(@NotNull String message) {
418     String path = myPath.toString();
419     StringBuilder builder = new StringBuilder();
420     builder.append("The ").append(myName).append(" (id=").append(myId).append(", path=");
421     builder.append(FileUtil.getLocationRelativeToUserHome(path, false));
422     if (myVersion != null && !isBundled() && !myVersion.equals(PluginManagerCore.getBuildNumber().asString())) {
423       builder.append(", version=").append(myVersion);
424     }
425     builder.append(") plugin ").append(message);
426     return builder.toString();
427   }
428
429   private void markAsIncomplete(@NotNull DescriptorLoadingContext context) {
430     incomplete = true;
431     setEnabled(false);
432     if (myId != null) {
433       context.parentContext.result.addIncompletePlugin(this);
434     }
435   }
436
437   private static @NotNull ServiceDescriptor readServiceDescriptor(@NotNull Element element,
438                                                                   @NotNull DescriptorListLoadingContext loadingContext) {
439     ServiceDescriptor descriptor = new ServiceDescriptor();
440     descriptor.serviceInterface = element.getAttributeValue("serviceInterface");
441     descriptor.serviceImplementation = StringUtil.nullize(element.getAttributeValue("serviceImplementation"));
442     descriptor.testServiceImplementation = StringUtil.nullize(element.getAttributeValue("testServiceImplementation"));
443     descriptor.headlessImplementation = StringUtil.nullize(element.getAttributeValue("headlessImplementation"));
444     descriptor.configurationSchemaKey = element.getAttributeValue("configurationSchemaKey");
445
446     String preload = element.getAttributeValue("preload");
447     if (preload != null) {
448       switch (preload) {
449         case "true":
450           descriptor.preload = ServiceDescriptor.PreloadMode.TRUE;
451           break;
452         case "await":
453           descriptor.preload = ServiceDescriptor.PreloadMode.AWAIT;
454           break;
455         case "notHeadless":
456           descriptor.preload = ServiceDescriptor.PreloadMode.NOT_HEADLESS;
457           break;
458         case "notLightEdit":
459           descriptor.preload = ServiceDescriptor.PreloadMode.NOT_LIGHT_EDIT;
460           break;
461         default:
462           loadingContext.getLogger().error("Unknown preload mode value: " + JDOMUtil.writeElement(element));
463           break;
464       }
465     }
466
467     descriptor.overrides = Boolean.parseBoolean(element.getAttributeValue("overrides"));
468     return descriptor;
469   }
470
471   private static void readComponents(@NotNull Element parent, @NotNull ContainerDescriptor containerDescriptor) {
472     List<Content> content = parent.getContent();
473     int contentSize = content.size();
474     if (contentSize == 0) {
475       return;
476     }
477
478     List<ComponentConfig> result = containerDescriptor.getComponentListToAdd(contentSize);
479     for (Content child : content) {
480       if (!(child instanceof Element)) {
481         continue;
482       }
483
484       Element componentElement = (Element)child;
485       if (!componentElement.getName().equals("component")) {
486         continue;
487       }
488
489       ComponentConfig componentConfig = new ComponentConfig();
490       Map<String, String> options = null;
491       loop:
492       for (Element elementChild : componentElement.getChildren()) {
493         switch (elementChild.getName()) {
494           case "skipForDefaultProject":
495             if (!readBoolValue(elementChild.getTextTrim())) {
496               componentConfig.setLoadForDefaultProject(true);
497             }
498             break;
499
500           case "loadForDefaultProject":
501             componentConfig.setLoadForDefaultProject(readBoolValue(elementChild.getTextTrim()));
502             break;
503
504           case "interface-class":
505             componentConfig.setInterfaceClass(elementChild.getTextTrim());
506             break;
507
508           case "implementation-class":
509             componentConfig.setImplementationClass(elementChild.getTextTrim());
510             break;
511
512           case "headless-implementation-class":
513             componentConfig.setHeadlessImplementationClass(elementChild.getTextTrim());
514             break;
515
516           case "option":
517             String name = elementChild.getAttributeValue("name");
518             String value = elementChild.getAttributeValue("value");
519             if (name != null) {
520               if (name.equals("os")) {
521                 if (value != null && !isSuitableForOs(value)) {
522                   continue loop;
523                 }
524               }
525               else {
526                 if (options == null) {
527                   options = Collections.singletonMap(name, value);
528                 }
529                 else {
530                   if (options.size() == 1) {
531                     options = new HashMap<>(options);
532                   }
533                   options.put(name, value);
534                 }
535               }
536             }
537             break;
538         }
539       }
540
541       if (options != null) {
542         componentConfig.options = options;
543       }
544
545       result.add(componentConfig);
546     }
547   }
548
549   private static boolean readBoolValue(@NotNull String value) {
550     return value.isEmpty() || value.equalsIgnoreCase("true");
551   }
552
553   @Nullable
554   private Date parseReleaseDate(@Nullable String dateStr, @NotNull DescriptorListLoadingContext context) {
555     if (StringUtil.isEmpty(dateStr)) {
556       return null;
557     }
558
559     try {
560       return context.getDateParser().parse(dateStr);
561     }
562     catch (ParseException e) {
563       context.getLogger().info("Error parse release date from plugin descriptor for plugin " + myName + " {" + myId + "}: " + e.getMessage());
564     }
565     return null;
566   }
567
568   public static final Pattern EXPLICIT_BIG_NUMBER_PATTERN = Pattern.compile("(.*)\\.(9{4,}+|10{4,}+)");
569
570   /**
571    * Convert build number like '146.9999' to '146.*' (like plugin repository does) to ensure that plugins which have such values in
572    * 'until-build' attribute will be compatible with 146.SNAPSHOT build.
573    */
574   public static String convertExplicitBigNumberInUntilBuildToStar(@Nullable String build) {
575     if (build == null) return null;
576     Matcher matcher = EXPLICIT_BIG_NUMBER_PATTERN.matcher(build);
577     if (matcher.matches()) {
578       return matcher.group(1) + ".*";
579     }
580     return build;
581   }
582
583   @ApiStatus.Internal
584   public void registerExtensionPoints(@NotNull ExtensionsAreaImpl area, @NotNull ComponentManager componentManager) {
585     ContainerDescriptor containerDescriptor;
586     boolean clonePoint = true;
587     if (componentManager.getPicoContainer().getParent() == null) {
588       containerDescriptor = myAppContainerDescriptor;
589       clonePoint = false;
590     }
591     else if (componentManager.getPicoContainer().getParent().getParent() == null) {
592       containerDescriptor = myProjectContainerDescriptor;
593     }
594     else {
595       containerDescriptor = myModuleContainerDescriptor;
596     }
597
598     List<ExtensionPointImpl<?>> extensionPoints = containerDescriptor.extensionPoints;
599     if (extensionPoints != null) {
600       area.registerExtensionPoints(extensionPoints, clonePoint);
601     }
602   }
603
604   @Nullable
605   private ContainerDescriptor getContainerDescriptorByExtensionArea(@Nullable String area) {
606     if (area == null) {
607       return myAppContainerDescriptor;
608     }
609     else if ("IDEA_PROJECT".equals(area)) {
610       return myProjectContainerDescriptor;
611     }
612     else if ("IDEA_MODULE".equals(area)) {
613       return myModuleContainerDescriptor;
614     }
615     else {
616       return null;
617     }
618   }
619
620   @NotNull
621   public ContainerDescriptor getAppContainerDescriptor() {
622     return myAppContainerDescriptor;
623   }
624
625   @NotNull
626   public ContainerDescriptor getProjectContainerDescriptor() {
627     return myProjectContainerDescriptor;
628   }
629
630   @NotNull
631   public ContainerDescriptor getModuleContainerDescriptor() {
632     return myModuleContainerDescriptor;
633   }
634
635   @ApiStatus.Internal
636   public void registerExtensions(@NotNull ExtensionsAreaImpl area, @NotNull ComponentManager componentManager, boolean notifyListeners) {
637     List<Runnable> listeners = notifyListeners ? new ArrayList<>() : null;
638     registerExtensions(area, componentManager, this, listeners);
639     if (listeners != null) {
640       listeners.forEach(Runnable::run);
641     }
642   }
643
644   @ApiStatus.Internal
645   public void registerExtensions(@NotNull ExtensionsAreaImpl area,
646                                  @NotNull ComponentManager componentManager,
647                                  @NotNull IdeaPluginDescriptorImpl rootDescriptor,
648                                  @Nullable List<Runnable> listenerCallbacks) {
649     THashMap<String, List<Element>> extensions;
650     if (componentManager.getPicoContainer().getParent() == null) {
651       extensions = myAppContainerDescriptor.extensions;
652       if (extensions == null) {
653         if (myExtensions == null) {
654           return;
655         }
656
657         myExtensions.retainEntries((name, list) -> {
658           if (area.registerExtensions(name, list, rootDescriptor, componentManager, listenerCallbacks)) {
659             if (myAppContainerDescriptor.extensions == null) {
660               myAppContainerDescriptor.extensions = new THashMap<>();
661             }
662             addExtensionList(myAppContainerDescriptor.extensions, name, list);
663             return false;
664           }
665           return true;
666         });
667         myExtensionsCleared = true;
668
669         if (myExtensions.isEmpty()) {
670           myExtensions = null;
671         }
672
673         return;
674       }
675       // else... it means that another application is created for the same set of plugins - at least, this case should be supported for tests
676     }
677     else {
678       extensions = myExtensions;
679       if (extensions == null) {
680         return;
681       }
682     }
683
684     extensions.forEachEntry((name, list) -> {
685       area.registerExtensions(name, list, rootDescriptor, componentManager, listenerCallbacks);
686       return true;
687     });
688   }
689
690   @Override
691   public String getDescription() {
692     String result = myDescription;
693     if (result != null) {
694       return result;
695     }
696
697     ResourceBundle bundle = null;
698     if (myResourceBundleBaseName != null) {
699       try {
700         bundle = DynamicBundle.INSTANCE.getResourceBundle(myResourceBundleBaseName, getPluginClassLoader());
701       }
702       catch (MissingResourceException e) {
703         PluginManagerCore.getLogger().info("Cannot find plugin " + myId + " resource-bundle: " + myResourceBundleBaseName);
704       }
705     }
706
707     if (bundle == null) {
708       result = myDescriptionChildText;
709     }
710     else {
711       result = AbstractBundle.messageOrDefault(bundle, "plugin." + myId + ".description", StringUtil.notNullize(myDescriptionChildText));
712     }
713     myDescription = result;
714     return result;
715   }
716
717   @Override
718   public String getChangeNotes() {
719     return myChangeNotes;
720   }
721
722   @Override
723   public String getName() {
724     return myName;
725   }
726
727   @Nullable
728   @Override
729   public String getProductCode() {
730     return myProductCode;
731   }
732
733   @Nullable
734   @Override
735   public Date getReleaseDate() {
736     return myReleaseDate;
737   }
738
739   @Override
740   public int getReleaseVersion() {
741     return myReleaseVersion;
742   }
743
744   public boolean isLicenseOptional() {
745     return myIsLicenseOptional;
746   }
747
748   @Override
749   public @NotNull List<PluginId> getIncompatibleModuleIds() {
750     return ContainerUtil.notNullize(myIncompatibilities);
751   }
752
753   @Override
754   public PluginId @NotNull [] getDependentPluginIds() {
755     return myDependencies;
756   }
757
758   @Override
759   public PluginId @NotNull [] getOptionalDependentPluginIds() {
760     return myOptionalDependencies;
761   }
762
763   @Override
764   public String getVendor() {
765     return myVendor;
766   }
767
768   @Override
769   public String getVersion() {
770     return myVersion;
771   }
772
773   @Override
774   public String getResourceBundleBaseName() {
775     return myResourceBundleBaseName;
776   }
777
778   @Override
779   public String getCategory() {
780     return myCategory;
781   }
782
783   /*
784      This setter was explicitly defined to be able to set a category for a
785      descriptor outside its loading from the xml file.
786      Problem was that most commonly plugin authors do not publish the plugin's
787      category in its .xml file so to be consistent in plugins representation
788      (e.g. in the Plugins form) we have to set this value outside.
789   */
790   public void setCategory(String category) {
791     myCategory = category;
792   }
793
794   @Nullable
795   public Map<String, List<Element>> getExtensions() {
796     if (myExtensionsCleared) {
797       throw new IllegalStateException("Trying to retrieve extensions list after extension elements have been cleared");
798     }
799     if (myExtensions == null) {
800       return null;
801     }
802     else {
803       Map<String, List<Element>> result = new THashMap<>(myExtensions.size());
804       result.putAll(myExtensions);
805       return result;
806     }
807   }
808
809   /**
810    * @deprecated Do not use. If you want to get class loader for own plugin, just use your current class's class loader.
811    */
812   @NotNull
813   @Deprecated
814   public List<File> getClassPath() {
815     File path = myPath.toFile();
816     if (!path.isDirectory()) {
817       return Collections.singletonList(path);
818     }
819
820     List<File> result = new ArrayList<>();
821     File classesDir = new File(path, "classes");
822     if (classesDir.exists()) {
823       result.add(classesDir);
824     }
825
826     File[] files = new File(path, "lib").listFiles();
827     if (files == null || files.length <= 0) {
828       return result;
829     }
830
831     for (File f : files) {
832       if (f.isFile()) {
833         String name = f.getName();
834         if (StringUtil.endsWithIgnoreCase(name, ".jar") || StringUtil.endsWithIgnoreCase(name, ".zip")) {
835           result.add(f);
836         }
837       }
838       else {
839         result.add(f);
840       }
841     }
842     return result;
843   }
844
845   @NotNull List<Path> collectClassPath() {
846     if (!Files.isDirectory(myPath)) {
847       return Collections.singletonList(myPath);
848     }
849
850     List<Path> result = new ArrayList<>();
851     Path classesDir = myPath.resolve("classes");
852     if (Files.exists(classesDir)) {
853       result.add(classesDir);
854     }
855
856     try (DirectoryStream<Path> childStream = Files.newDirectoryStream(myPath.resolve("lib"))) {
857       for (Path f : childStream) {
858         if (Files.isRegularFile(f)) {
859           String name = f.getFileName().toString();
860           if (StringUtilRt.endsWithIgnoreCase(name, ".jar") || StringUtilRt.endsWithIgnoreCase(name, ".zip")) {
861             result.add(f);
862           }
863         }
864         else {
865           result.add(f);
866         }
867       }
868     }
869     catch (NoSuchFileException ignore) {
870     }
871     catch (IOException e) {
872       PluginManagerCore.getLogger().debug(e);
873     }
874     return result;
875   }
876
877   @Override
878   @Nullable
879   public List<Element> getActionDescriptionElements() {
880     return myActionElements;
881   }
882
883   @Override
884   public String getVendorEmail() {
885     return myVendorEmail;
886   }
887
888   @Override
889   public String getVendorUrl() {
890     return myVendorUrl;
891   }
892
893   @Override
894   public String getUrl() {
895     return myUrl;
896   }
897
898   public void setUrl(String val) {
899     myUrl = val;
900   }
901
902   public boolean isDeleted() {
903     return myDeleted;
904   }
905
906   public void setDeleted(boolean deleted) {
907     myDeleted = deleted;
908   }
909
910   public void setLoader(@Nullable ClassLoader loader) {
911     myLoader = loader;
912   }
913
914   public boolean unloadClassLoader() {
915     GCWatcher watcher = GCWatcher.tracking(myLoader);
916     myLoader = null;
917     return watcher.tryCollect();
918   }
919
920   @Override
921   public PluginId getPluginId() {
922     return myId;
923   }
924
925   @Override
926   public ClassLoader getPluginClassLoader() {
927     return myLoader != null ? myLoader : getClass().getClassLoader();
928   }
929
930   public boolean getUseIdeaClassLoader() {
931     return myUseIdeaClassLoader;
932   }
933
934   boolean isUseCoreClassLoader() {
935     return myUseCoreClassLoader;
936   }
937
938   void setUseCoreClassLoader() {
939     myUseCoreClassLoader = true;
940   }
941
942   @Override
943   public boolean isEnabled() {
944     return myEnabled;
945   }
946
947   @Override
948   public void setEnabled(final boolean enabled) {
949     myEnabled = enabled;
950   }
951
952   @Override
953   public Disposable getPluginDisposable() {
954     return PluginManager.pluginDisposables.get(this);
955   }
956
957   @Override
958   public String getSinceBuild() {
959     return mySinceBuild;
960   }
961
962   @Override
963   public String getUntilBuild() {
964     return myUntilBuild;
965   }
966
967   void mergeOptionalConfig(@NotNull IdeaPluginDescriptorImpl descriptor) {
968     if (myExtensions == null) {
969       myExtensions = descriptor.myExtensions;
970     }
971     else if (descriptor.myExtensions != null) {
972       descriptor.myExtensions.forEachEntry((name, list) -> {
973         addExtensionList(myExtensions, name, list);
974         return true;
975       });
976     }
977
978     if (myActionElements == null) {
979       myActionElements = descriptor.myActionElements;
980     }
981     else if (descriptor.myActionElements != null) {
982       myActionElements.addAll(descriptor.myActionElements);
983     }
984
985     myAppContainerDescriptor.merge(descriptor.myAppContainerDescriptor);
986     myProjectContainerDescriptor.merge(descriptor.myProjectContainerDescriptor);
987     myModuleContainerDescriptor.merge(descriptor.myModuleContainerDescriptor);
988   }
989
990   private static void addExtensionList(@NotNull Map<String, List<Element>> map, @NotNull String name, @NotNull List<Element> list) {
991     List<Element> myList = map.get(name);
992     if (myList == null) {
993       map.put(name, list);
994     }
995     else {
996       myList.addAll(list);
997     }
998   }
999
1000   @Override
1001   public boolean isBundled() {
1002     return myBundled;
1003   }
1004
1005   @Override
1006   public boolean allowBundledUpdate() {
1007     return myAllowBundledUpdate;
1008   }
1009
1010   @Override
1011   public boolean isImplementationDetail() {
1012     return myImplementationDetail;
1013   }
1014
1015   @NotNull
1016   public List<PluginId> getModules() {
1017     return ContainerUtil.notNullize(myModules);
1018   }
1019
1020   @Override
1021   public boolean equals(Object o) {
1022     return this == o || o instanceof IdeaPluginDescriptorImpl && myId == ((IdeaPluginDescriptorImpl)o).myId;
1023   }
1024
1025   @Override
1026   public int hashCode() {
1027     return Objects.hashCode(myId);
1028   }
1029
1030   @Override
1031   public String toString() {
1032     return "PluginDescriptor(name=" + myName + ", id=" + myId + ", path=" + myPath + ")";
1033   }
1034
1035   @SuppressWarnings("BooleanMethodIsAlwaysInverted")
1036   private static boolean isSuitableForOs(@NotNull String os) {
1037     if (os.isEmpty()) {
1038       return true;
1039     }
1040
1041     if (os.equals(OS.mac.name())) {
1042       return SystemInfo.isMac;
1043     }
1044     else if (os.equals(OS.linux.name())) {
1045       return SystemInfo.isLinux;
1046     }
1047     else if (os.equals(OS.windows.name())) {
1048       return SystemInfo.isWindows;
1049     }
1050     else if (os.equals(OS.unix.name())) {
1051       return SystemInfo.isUnix;
1052     }
1053     else if (os.equals(OS.freebsd.name())) {
1054       return SystemInfo.isFreeBSD;
1055     }
1056     else {
1057       throw new IllegalArgumentException("Unknown OS '" + os + "'");
1058     }
1059   }
1060
1061   private static final class PluginDependency {
1062     public PluginId pluginId;
1063     public boolean optional;
1064     public boolean available;
1065     public String configFile;
1066
1067     // maybe null if not yet read (as we read in parallel)
1068     public IdeaPluginDescriptorImpl dependency;
1069   }
1070
1071   @Nullable
1072   public String findOptionalDependencyConfigFile(@NotNull PluginId pluginId) {
1073     if (myDependencies == null) {
1074       return null;
1075     }
1076     for (PluginDependency dependency : myPluginDependencies) {
1077       if (dependency.pluginId.equals(pluginId)) {
1078         return dependency.configFile;
1079       }
1080     }
1081     return null;
1082   }
1083
1084   private static final class XmlReader {
1085     static void readListeners(@NotNull IdeaPluginDescriptorImpl descriptor, @NotNull Element list, @NotNull ContainerDescriptor containerDescriptor) {
1086       List<Content> content = list.getContent();
1087       List<ListenerDescriptor> result = containerDescriptor.listeners;
1088       if (result == null) {
1089         result = new ArrayList<>(content.size());
1090         containerDescriptor.listeners = result;
1091       }
1092       else {
1093         ((ArrayList<ListenerDescriptor>)result).ensureCapacity(result.size() + content.size());
1094       }
1095
1096       for (Content item : content) {
1097         if (!(item instanceof Element)) {
1098           continue;
1099         }
1100
1101         Element child = (Element)item;
1102
1103         String os = child.getAttributeValue("os");
1104         if (os != null && !isSuitableForOs(os)) {
1105           continue;
1106         }
1107
1108         String listenerClassName = child.getAttributeValue("class");
1109         String topicClassName = child.getAttributeValue("topic");
1110         if (listenerClassName == null || topicClassName == null) {
1111           PluginManagerCore.getLogger().error("Listener descriptor is not correct: " + JDOMUtil.writeElement(child));
1112         }
1113         else {
1114           result.add(new ListenerDescriptor(listenerClassName, topicClassName,
1115                                             getBoolean("activeInTestMode", child), getBoolean("activeInHeadlessMode", child), descriptor));
1116         }
1117       }
1118     }
1119
1120     static void readIdAndName(@NotNull IdeaPluginDescriptorImpl descriptor, @NotNull Element element) {
1121       String idString = descriptor.myId == null ? element.getChildTextTrim("id") : descriptor.myId.getIdString();
1122       String name = element.getChildTextTrim("name");
1123       if (idString == null) {
1124         idString = name;
1125       }
1126       else if (name == null) {
1127         name = idString;
1128       }
1129
1130       descriptor.myName = name;
1131       if (descriptor.myId == null) {
1132         descriptor.myId = StringUtil.isEmpty(idString) ? null : PluginId.getId(idString);
1133       }
1134     }
1135
1136     static void readMetaInfo(@NotNull IdeaPluginDescriptorImpl descriptor, @NotNull Element element) {
1137       if (!element.hasAttributes()) {
1138         return;
1139       }
1140
1141       List<Attribute> attributes = element.getAttributes();
1142       for (Attribute attribute : attributes) {
1143         switch (attribute.getName()) {
1144           case "url":
1145             descriptor.myUrl = StringUtil.nullize(attribute.getValue());
1146             break;
1147
1148           case "use-idea-classloader":
1149             descriptor.myUseIdeaClassLoader = Boolean.parseBoolean(attribute.getValue());
1150             break;
1151
1152           case "allow-bundled-update":
1153             descriptor.myAllowBundledUpdate = Boolean.parseBoolean(attribute.getValue());
1154             break;
1155
1156           case "implementation-detail":
1157             descriptor.myImplementationDetail = Boolean.parseBoolean(attribute.getValue());
1158             break;
1159
1160           case "version":
1161             String internalVersionString = StringUtil.nullize(attribute.getValue());
1162             if (internalVersionString != null) {
1163               try {
1164                 Integer.parseInt(internalVersionString);
1165               }
1166               catch (NumberFormatException e) {
1167                 PluginManagerCore.getLogger().error(new PluginException("Invalid value in plugin.xml format version: '" + internalVersionString + "'", e, descriptor.myId));
1168               }
1169             }
1170             break;
1171         }
1172       }
1173     }
1174
1175     static int collapseDuplicateDependencies(List<PluginDependency> dependencies) {
1176       int size = 0;
1177       for (int i = 0, n = dependencies.size(); i < n; i++) {
1178         PluginDependency dependency = dependencies.get(i);
1179         size++;
1180         if (!dependency.available || !dependency.optional) {
1181           continue;
1182         }
1183
1184         for (int j = 0; j < i; j++) {
1185           PluginDependency prev = dependencies.get(j);
1186           if (prev != null && !prev.optional && prev.pluginId == dependency.pluginId) {
1187             dependency.optional = false;
1188             dependencies.set(j, null);
1189             size--;
1190             break;
1191           }
1192         }
1193       }
1194       return size;
1195     }
1196
1197     static <T> void readDependencies(@NotNull IdeaPluginDescriptorImpl rootDescriptor,
1198                                      @NotNull IdeaPluginDescriptorImpl descriptor,
1199                                      @NotNull DescriptorLoadingContext context,
1200                                      @NotNull PathBasedJdomXIncluder.PathResolver<T> pathResolver) {
1201       List<String> visitedFiles = null;
1202       List<PluginDependency> dependencies = descriptor.myPluginDependencies;
1203
1204       // https://youtrack.jetbrains.com/issue/IDEA-206274
1205       int size = collapseDuplicateDependencies(dependencies);
1206
1207       for (PluginDependency dependency : dependencies) {
1208         if (dependency == null) continue;
1209
1210         // because of https://youtrack.jetbrains.com/issue/IDEA-206274, configFile maybe not only for optional dependencies
1211         String configFile = dependency.configFile;
1212         if (configFile == null) {
1213           continue;
1214         }
1215
1216         Element element;
1217         try {
1218           element = pathResolver.resolvePath(descriptor.myBasePath, configFile, context.parentContext.getXmlFactory());
1219         }
1220         catch (IOException | JDOMException e) {
1221           context.parentContext.getLogger().info("Plugin " + rootDescriptor.getPluginId() + " misses optional descriptor " + configFile);
1222           continue;
1223         }
1224
1225         if (visitedFiles == null) {
1226           visitedFiles = context.parentContext.getVisitedFiles();
1227         }
1228
1229         checkCycle(rootDescriptor, configFile, visitedFiles);
1230
1231         IdeaPluginDescriptorImpl tempDescriptor;
1232         IdeaPluginDescriptorImpl dependencyDescriptor = dependency.dependency;
1233         if (dependencyDescriptor != null && context.parentContext.readConditionalConfigDirectlyIfPossible) {
1234           // read directly into effective descriptor
1235           tempDescriptor = rootDescriptor;
1236         }
1237         else {
1238           // effective descriptor cannot be used because not yet clear, is dependency resolvable or not
1239           tempDescriptor = new IdeaPluginDescriptorImpl(descriptor.myPath, false);
1240         }
1241
1242         visitedFiles.add(configFile);
1243         if (!tempDescriptor.readExternal(element, descriptor.myBasePath, pathResolver, context, rootDescriptor)) {
1244           tempDescriptor = null;
1245         }
1246         visitedFiles.clear();
1247
1248         if (tempDescriptor != null) {
1249           if (descriptor.optionalConfigs == null) {
1250             descriptor.optionalConfigs = new LinkedHashMap<>();
1251           }
1252           ContainerUtilRt.putValue(dependency.pluginId, tempDescriptor, descriptor.optionalConfigs);
1253         }
1254       }
1255
1256       collectDependentPluginIds(descriptor, size);
1257     }
1258
1259     static void collectDependentPluginIds(@NotNull IdeaPluginDescriptorImpl descriptor, int dependencyListSize) {
1260       List<PluginDependency> dependencies = descriptor.myPluginDependencies;
1261       PluginId[] dependentPlugins = new PluginId[dependencyListSize];
1262       int optionalSize = 0;
1263       int index = 0;
1264       for (PluginDependency dependency : dependencies) {
1265         if (dependency == null) {
1266           continue;
1267         }
1268
1269         dependentPlugins[index++] = dependency.pluginId;
1270         if (dependency.optional) {
1271           optionalSize++;
1272         }
1273       }
1274
1275       descriptor.myDependencies = dependentPlugins;
1276
1277       if (optionalSize > 0) {
1278         if (optionalSize == dependentPlugins.length) {
1279           descriptor.myOptionalDependencies = dependentPlugins;
1280         }
1281         else {
1282           PluginId[] optionalDependencies = new PluginId[optionalSize];
1283           index = 0;
1284           for (PluginDependency dependency : dependencies) {
1285             if (dependency == null) {
1286               continue;
1287             }
1288             if (dependency.optional) {
1289               optionalDependencies[index++] = dependency.pluginId;
1290             }
1291           }
1292           descriptor.myOptionalDependencies = optionalDependencies;
1293         }
1294       }
1295     }
1296
1297     private static void checkCycle(@NotNull IdeaPluginDescriptorImpl rootDescriptor,
1298                                    @NotNull String configFile,
1299                                    @NotNull List<String> visitedFiles) {
1300       for (int i = 0, n = visitedFiles.size(); i < n; i++) {
1301         if (configFile.equals(visitedFiles.get(i))) {
1302           List<String> cycle = visitedFiles.subList(i, visitedFiles.size());
1303           PluginId pluginId = rootDescriptor.getPluginId();
1304           throw new RuntimeException("Plugin " + pluginId + " optional descriptors form a cycle: " + String.join(", ", cycle));
1305         }
1306       }
1307     }
1308
1309     private static boolean getBoolean(@NotNull String name, @NotNull Element child) {
1310       String value = child.getAttributeValue(name);
1311       return value == null || Boolean.parseBoolean(value);
1312     }
1313
1314     static void readExtensions(@NotNull IdeaPluginDescriptorImpl descriptor, DescriptorListLoadingContext loadingContext, Element child) {
1315       String ns = child.getAttributeValue("defaultExtensionNs");
1316       THashMap<String, List<Element>> epNameToExtensions = descriptor.myExtensions;
1317       Interner<String> stringInterner = loadingContext.getStringInterner();
1318       for (Element extensionElement : child.getChildren()) {
1319         String os = extensionElement.getAttributeValue("os");
1320         if (os != null) {
1321           extensionElement.removeAttribute("os");
1322           if (!isSuitableForOs(os)) {
1323             continue;
1324           }
1325         }
1326
1327         String qualifiedExtensionPointName = stringInterner.intern(ExtensionsAreaImpl.extractPointName(extensionElement, ns));
1328         ContainerDescriptor containerDescriptor;
1329         switch (qualifiedExtensionPointName) {
1330           case APPLICATION_SERVICE:
1331             containerDescriptor = descriptor.myAppContainerDescriptor;
1332             break;
1333           case PROJECT_SERVICE:
1334             containerDescriptor = descriptor.myProjectContainerDescriptor;
1335             break;
1336           case MODULE_SERVICE:
1337             containerDescriptor = descriptor.myModuleContainerDescriptor;
1338             break;
1339           default:
1340             if (epNameToExtensions == null) {
1341               epNameToExtensions = new THashMap<>();
1342               descriptor.myExtensions = epNameToExtensions;
1343             }
1344
1345             List<Element> list = epNameToExtensions.get(qualifiedExtensionPointName);
1346             if (list == null) {
1347               list = new SmartList<>();
1348               epNameToExtensions.put(qualifiedExtensionPointName, list);
1349             }
1350             list.add(extensionElement);
1351             continue;
1352         }
1353
1354         containerDescriptor.addService(readServiceDescriptor(extensionElement, loadingContext));
1355       }
1356     }
1357
1358     /**
1359      * EP cannot be added directly to root descriptor, because probably later EP list will be ignored if dependency plugin is not available.
1360      * So, we use rootDescriptor as plugin id (because descriptor plugin id is null - it is not plugin descriptor, but optional config descriptor)
1361      * and for BeanExtensionPoint/InterfaceExtensionPoint (because instances will be used only if merged).
1362      *
1363      * And descriptor as data container.
1364      */
1365     static void readExtensionPoints(@NotNull IdeaPluginDescriptorImpl rootDescriptor, @NotNull IdeaPluginDescriptorImpl descriptor, @NotNull Element parentElement) {
1366       for (Content child : parentElement.getContent()) {
1367         if (!(child instanceof Element)) {
1368           continue;
1369         }
1370
1371         Element element = (Element)child;
1372
1373         String area = element.getAttributeValue(ExtensionsAreaImpl.ATTRIBUTE_AREA);
1374         ContainerDescriptor containerDescriptor = descriptor.getContainerDescriptorByExtensionArea(area);
1375         if (containerDescriptor == null) {
1376           PluginManagerCore.getLogger().error("Unknown area: " + area);
1377           continue;
1378         }
1379
1380         String pointName = getExtensionPointName(element, rootDescriptor.getPluginId());
1381
1382         String beanClassName = element.getAttributeValue("beanClass");
1383         String interfaceClassName = element.getAttributeValue("interface");
1384         if (beanClassName == null && interfaceClassName == null) {
1385           throw new RuntimeException("Neither 'beanClass' nor 'interface' attribute is specified for extension point '" + pointName + "' in '" + rootDescriptor.getPluginId() + "' plugin");
1386         }
1387
1388         if (beanClassName != null && interfaceClassName != null) {
1389           throw new RuntimeException("Both 'beanClass' and 'interface' attributes are specified for extension point '" + pointName + "' in '" + rootDescriptor.getPluginId() + "' plugin");
1390         }
1391
1392         List<ExtensionPointImpl<?>> result = containerDescriptor.extensionPoints;
1393         if (result == null) {
1394           result = new ArrayList<>();
1395           containerDescriptor.extensionPoints = result;
1396         }
1397
1398         boolean dynamic = Boolean.parseBoolean(element.getAttributeValue("dynamic"));
1399         ExtensionPointImpl<Object> point;
1400         if (interfaceClassName == null) {
1401           point = new BeanExtensionPoint<>(pointName, beanClassName, rootDescriptor, dynamic);
1402         }
1403         else {
1404           point = new InterfaceExtensionPoint<>(pointName, interfaceClassName, rootDescriptor, dynamic);
1405         }
1406
1407         result.add(point);
1408       }
1409     }
1410
1411     @NotNull
1412     private static String getExtensionPointName(@NotNull Element extensionPointElement, @NotNull PluginId effectivePluginId) {
1413       String pointName = extensionPointElement.getAttributeValue("qualifiedName");
1414       if (pointName == null) {
1415         String name = extensionPointElement.getAttributeValue("name");
1416         if (name == null) {
1417           throw new RuntimeException("'name' attribute not specified for extension point in '" + effectivePluginId + "' plugin");
1418         }
1419
1420         pointName = effectivePluginId.getIdString() + '.' + name;
1421       }
1422       return pointName;
1423     }
1424   }
1425 }