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