92 += 2
[idea/community.git] / platform / platform-api / src / com / intellij / ide / plugins / PluginManager.java
1 /*
2  * Copyright 2000-2009 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package com.intellij.ide.plugins;
18
19 import com.intellij.ide.ClassloaderUtil;
20 import com.intellij.ide.IdeBundle;
21 import com.intellij.ide.plugins.cl.PluginClassLoader;
22 import com.intellij.idea.Main;
23 import com.intellij.openapi.application.Application;
24 import com.intellij.openapi.application.ApplicationManager;
25 import com.intellij.openapi.application.PathManager;
26 import com.intellij.openapi.application.impl.PluginsFacade;
27 import com.intellij.openapi.diagnostic.Logger;
28 import com.intellij.openapi.extensions.Extensions;
29 import com.intellij.openapi.extensions.LogProvider;
30 import com.intellij.openapi.extensions.PluginId;
31 import com.intellij.openapi.progress.ProcessCanceledException;
32 import com.intellij.openapi.util.BuildNumber;
33 import com.intellij.openapi.util.Comparing;
34 import com.intellij.openapi.util.Condition;
35 import com.intellij.openapi.util.io.FileUtil;
36 import com.intellij.openapi.util.text.StringUtil;
37 import com.intellij.util.ArrayUtil;
38 import com.intellij.util.Function;
39 import com.intellij.util.graph.CachingSemiGraph;
40 import com.intellij.util.graph.DFSTBuilder;
41 import com.intellij.util.graph.Graph;
42 import com.intellij.util.graph.GraphGenerator;
43 import com.intellij.util.lang.UrlClassLoader;
44 import com.intellij.util.xmlb.XmlSerializationException;
45 import gnu.trove.THashMap;
46 import org.jetbrains.annotations.NonNls;
47 import org.jetbrains.annotations.Nullable;
48 import sun.reflect.Reflection;
49
50 import javax.swing.*;
51 import java.io.*;
52 import java.lang.reflect.InvocationTargetException;
53 import java.lang.reflect.Method;
54 import java.net.*;
55 import java.util.*;
56
57 /**
58  * @author mike
59  */
60 @SuppressWarnings({"UseOfSystemOutOrSystemErr", "CallToPrintStackTrace"}) // No logger is loaded at this time so we have to use these.
61 public class PluginManager {
62
63   @NonNls public static final String AREA_IDEA_PROJECT = "IDEA_PROJECT";
64   @NonNls public static final String AREA_IDEA_MODULE = "IDEA_MODULE";
65   @NonNls private static final String PROPERTY_PLUGIN_PATH = "plugin.path";
66   private static final Object PLUGIN_CLASSES_LOCK = new Object();
67   private static String myPluginError = null;
68   @NonNls public static final String CORE_PLUGIN_ID = "com.intellij";
69
70   @NonNls public static final String DISABLED_PLUGINS_FILENAME = "disabled_plugins.txt";
71
72   private static List<String> ourDisabledPlugins = null;
73
74   static final Object lock = new Object();
75
76   private static BuildNumber ourBuildNumber;
77   @NonNls public static final String PLUGIN_XML = "plugin.xml";
78   @NonNls public static final String META_INF = "META-INF";
79   private static final Map<PluginId,Integer> ourId2Index = new THashMap<PluginId, Integer>();
80   @NonNls private static final String MODULE_DEPENDENCY_PREFIX = "com.intellij.module";
81   private static final List<String> ourAvailableModules = new ArrayList<String>();
82
83   public static long startupStart;
84
85   public static class Facade extends PluginsFacade {
86     public IdeaPluginDescriptor getPlugin(PluginId id) {
87       return PluginManager.getPlugin(id);
88     }
89
90     public IdeaPluginDescriptor[] getPlugins() {
91       return PluginManager.getPlugins();
92     }
93   }
94
95   private static IdeaPluginDescriptorImpl[] ourPlugins;
96   private static Map<String, PluginId> ourPluginClasses;
97
98   /**
99    * do not call this method during bootstrap, should be called in a copy of PluginManager, loaded by IdeaClassLoader
100    */
101   public synchronized static IdeaPluginDescriptor[] getPlugins() {
102     if (ourPlugins == null) {
103       initializePlugins();
104       getLogger().info("Loaded plugins:" + StringUtil.join(ourPlugins, new Function<IdeaPluginDescriptorImpl, String>() {
105         public String fun(IdeaPluginDescriptorImpl descriptor) {
106           final String version = descriptor.getVersion();
107           return descriptor.getName() + (version != null ? " (" + version + ")" : "");
108         }
109       }, ", "));
110       ClassloaderUtil.clearJarURLCache();
111     }
112     return ourPlugins;
113   }
114
115   public static void invalidatePlugins() {
116     ourPlugins = null;
117     ourDisabledPlugins = null;
118   }
119
120   /**
121    * Called via reflection
122    */
123   @SuppressWarnings({"UnusedDeclaration"})
124   protected static void start(final String mainClass, final String methodName, final String[] args) {
125     startupStart = System.nanoTime();
126     try {
127       //noinspection HardCodedStringLiteral
128       ThreadGroup threadGroup = new ThreadGroup("Idea Thread Group") {
129         public void uncaughtException(Thread t, Throwable e) {
130           if (!(e instanceof ProcessCanceledException)) {
131             getLogger().error(e);
132           }
133         }
134       };
135
136       Runnable runnable = new Runnable() {
137         public void run() {
138           try {
139             ClassloaderUtil.clearJarURLCache();
140
141             //noinspection AssignmentToStaticFieldFromInstanceMethod
142             PluginsFacade.INSTANCE = new PluginManager.Facade();
143
144             Class aClass = Class.forName(mainClass);
145             final Method method = aClass.getDeclaredMethod(methodName, (ArrayUtil.EMPTY_STRING_ARRAY).getClass());
146             method.setAccessible(true);
147
148             //noinspection RedundantArrayCreation
149             method.invoke(null, new Object[]{args});
150           }
151           catch (Exception e) {
152             e.printStackTrace();
153             getLogger().error("Error while accessing " + mainClass + "." + methodName + " with arguments: " + Arrays.asList(args), e);
154           }
155         }
156       };
157
158       //noinspection HardCodedStringLiteral
159       new Thread(threadGroup, runnable, "Idea Main Thread").start();
160     }
161     catch (Exception e) {
162       getLogger().error(e);
163     }
164   }
165
166   private static void initializePlugins() {
167     configureExtensions();
168     
169     final IdeaPluginDescriptorImpl[] pluginDescriptors = loadDescriptors();
170
171     final Class callerClass = Reflection.getCallerClass(1);
172     final ClassLoader parentLoader = callerClass.getClassLoader();
173
174     final List<IdeaPluginDescriptorImpl> result = new ArrayList<IdeaPluginDescriptorImpl>();
175     for (IdeaPluginDescriptorImpl descriptor : pluginDescriptors) {
176       if (descriptor.getPluginId().getIdString().equals(CORE_PLUGIN_ID)) {
177         final List<String> modules = descriptor.getModules();
178         if (modules != null) {
179           ourAvailableModules.addAll(modules);
180         }
181       }
182
183       if (!shouldSkipPlugin(descriptor, pluginDescriptors)) {
184         result.add(descriptor);
185       }
186       else {
187         descriptor.setEnabled(false);
188         initClassLoader(parentLoader, descriptor);
189       }
190     }
191
192     prepareLoadingPluginsErrorMessage(filterBadPlugins(result));
193
194     final Map<PluginId, IdeaPluginDescriptorImpl> idToDescriptorMap = new HashMap<PluginId, IdeaPluginDescriptorImpl>();
195     for (final IdeaPluginDescriptorImpl descriptor : result) {
196       idToDescriptorMap.put(descriptor.getPluginId(), descriptor);
197     }
198
199     final IdeaPluginDescriptor corePluginDescriptor = idToDescriptorMap.get(PluginId.getId(CORE_PLUGIN_ID));
200     assert corePluginDescriptor != null;
201     for (IdeaPluginDescriptorImpl descriptor : result) {
202       if (descriptor != corePluginDescriptor) {
203         descriptor.insertDependency(corePluginDescriptor);
204       }
205     }
206
207     mergeOptionalConfigs(idToDescriptorMap);
208
209     // sort descriptors according to plugin dependencies
210     Collections.sort(result, getPluginDescriptorComparator(idToDescriptorMap));
211
212     for (int i = 0; i < result.size(); i++) {
213       ourId2Index.put(result.get(i).getPluginId(), i);
214     }
215
216     for (final IdeaPluginDescriptorImpl pluginDescriptor : result) {
217       if (pluginDescriptor.getPluginId().getIdString().equals(CORE_PLUGIN_ID) || pluginDescriptor.isUseCoreClassLoader()) {
218         pluginDescriptor.setLoader(parentLoader, true);
219       }
220       else {
221         final List<File> classPath = pluginDescriptor.getClassPath();
222         final PluginId[] dependentPluginIds = pluginDescriptor.getDependentPluginIds();
223         final ClassLoader[] parentLoaders = getParentLoaders(idToDescriptorMap, dependentPluginIds);
224
225         final ClassLoader pluginClassLoader = createPluginClassLoader(classPath.toArray(new File[classPath.size()]),
226                                                                       parentLoaders.length > 0 ? parentLoaders : new ClassLoader[] {parentLoader},
227                                                                       pluginDescriptor);
228         pluginDescriptor.setLoader(pluginClassLoader, true);
229       }
230
231       pluginDescriptor.registerExtensions();
232     }
233
234     ourPlugins = pluginDescriptors;
235   }
236
237   public static void initClassLoader(final ClassLoader parentLoader, final IdeaPluginDescriptorImpl descriptor) {
238     final List<File> classPath = descriptor.getClassPath();
239     final ClassLoader loader =
240         createPluginClassLoader(classPath.toArray(new File[classPath.size()]), new ClassLoader[]{parentLoader}, descriptor);
241     descriptor.setLoader(loader, false);
242   }
243
244   public static int getPluginLoadingOrder(PluginId id) {
245     return ourId2Index.get(id);
246   }
247
248   private static void mergeOptionalConfigs(Map<PluginId, IdeaPluginDescriptorImpl> descriptors) {
249     final Map<PluginId, IdeaPluginDescriptorImpl> descriptorsWithModules = new HashMap<PluginId, IdeaPluginDescriptorImpl>(descriptors);
250     addModulesAsDependents(descriptorsWithModules);
251     for (IdeaPluginDescriptorImpl descriptor : descriptors.values()) {
252       final Map<PluginId, IdeaPluginDescriptorImpl> optionalDescriptors = descriptor.getOptionalDescriptors();
253       if (optionalDescriptors != null && !optionalDescriptors.isEmpty()) {
254         for (Map.Entry<PluginId, IdeaPluginDescriptorImpl> entry: optionalDescriptors.entrySet()) {
255           if (descriptorsWithModules.containsKey(entry.getKey())) {
256             descriptor.mergeOptionalConfig(entry.getValue());  
257           }
258         }
259       }
260     }
261   }
262
263   private static void prepareLoadingPluginsErrorMessage(final String errorMessage) {
264     if (errorMessage != null) {
265       if (!Main.isHeadless() && !ApplicationManager.getApplication().isUnitTestMode()) {
266         if (myPluginError == null) {
267           myPluginError = errorMessage;
268         }
269         else {
270           myPluginError += "\n" + errorMessage;
271         }
272       } else {
273         getLogger().error(errorMessage);
274       }
275     }
276   }
277
278   private static void configureExtensions() {
279     Extensions.setLogProvider(new IdeaLogProvider());
280     Extensions.registerAreaClass(AREA_IDEA_PROJECT, null);
281     Extensions.registerAreaClass(AREA_IDEA_MODULE, AREA_IDEA_PROJECT);
282   }
283
284   private static boolean shouldLoadPlugins() {
285     try {
286       // no plugins during bootstrap
287       Class.forName("com.intellij.openapi.extensions.Extensions");
288     }
289     catch (ClassNotFoundException e) {
290       return false;
291     }
292     //noinspection HardCodedStringLiteral
293     final String loadPlugins = System.getProperty("idea.load.plugins");
294     return loadPlugins == null || Boolean.TRUE.toString().equals(loadPlugins);
295   }
296
297   public static boolean shouldSkipPlugin(final IdeaPluginDescriptor descriptor) {
298     if (descriptor instanceof IdeaPluginDescriptorImpl) {
299       IdeaPluginDescriptorImpl descriptorImpl = (IdeaPluginDescriptorImpl)descriptor;
300       Boolean skipped = descriptorImpl.getSkipped();
301       if (skipped != null) {
302         return skipped.booleanValue();
303       }
304       boolean result = shouldSkipPlugin(descriptor, ourPlugins);
305       descriptorImpl.setSkipped(result);
306       return result;
307     }
308     return shouldSkipPlugin(descriptor, ourPlugins);
309   }
310
311   private static boolean shouldSkipPlugin(final IdeaPluginDescriptor descriptor, IdeaPluginDescriptor[] loaded) {
312     final String idString = descriptor.getPluginId().getIdString();
313     if (idString.equals(CORE_PLUGIN_ID)) {
314       return false;
315     }
316
317     //noinspection HardCodedStringLiteral
318     final String pluginId = System.getProperty("idea.load.plugins.id");
319     if (pluginId == null) {
320       if (descriptor instanceof IdeaPluginDescriptorImpl && !((IdeaPluginDescriptorImpl)descriptor).isEnabled()) return true;
321
322       if (!shouldLoadPlugins()) return true;
323     }
324
325     final boolean checkModuleDependencies = !ourAvailableModules.isEmpty() && !ourAvailableModules.contains("com.intellij.modules.all");
326     if (checkModuleDependencies && !hasModuleDependencies(descriptor)) {
327       return true;
328     }
329
330     boolean shouldLoad;
331     //noinspection HardCodedStringLiteral
332     final String loadPluginCategory = System.getProperty("idea.load.plugins.category");
333     if (loadPluginCategory != null) {
334       shouldLoad = loadPluginCategory.equals(descriptor.getCategory());
335     }
336     else {
337       if (pluginId != null) {
338         shouldLoad = pluginId.equals(idString);
339         if (!shouldLoad) {
340           Map<PluginId,IdeaPluginDescriptor> map = new HashMap<PluginId, IdeaPluginDescriptor>();
341           for (final IdeaPluginDescriptor pluginDescriptor : loaded) {
342             map.put(pluginDescriptor.getPluginId(), pluginDescriptor);
343           }
344           addModulesAsDependents(map);
345           final IdeaPluginDescriptor descriptorFromProperty = map.get(PluginId.getId(pluginId));
346           shouldLoad = descriptorFromProperty != null && isDependent(descriptorFromProperty, descriptor.getPluginId(), map,
347                                                                      checkModuleDependencies);
348         }
349       } else {
350         shouldLoad = !getDisabledPlugins().contains(idString);
351       }
352       if (shouldLoad && descriptor instanceof IdeaPluginDescriptorImpl) {
353         if (isIncompatible(descriptor)) return true;
354       }
355     }
356
357     return !shouldLoad;
358   }
359
360   private static <T extends IdeaPluginDescriptor> void addModulesAsDependents(final Map<PluginId, T> map) {
361     for (String module : ourAvailableModules) {
362       // fake plugin descriptors to satisfy dependencies
363       map.put(PluginId.getId(module), (T) new IdeaPluginDescriptorImpl(null));
364     }
365   }
366
367   private static boolean hasModuleDependencies(final IdeaPluginDescriptor descriptor) {
368     final PluginId[] dependentPluginIds = descriptor.getDependentPluginIds();
369     for (PluginId dependentPluginId : dependentPluginIds) {
370       if (isModuleDependency(dependentPluginId)) {
371         return true;
372       }
373     }
374     return false;
375   }
376
377   public static boolean isModuleDependency(final PluginId dependentPluginId) {
378     return dependentPluginId.getIdString().startsWith(MODULE_DEPENDENCY_PREFIX);
379   }
380
381   private static boolean isDependent(final IdeaPluginDescriptor descriptor,
382                                      final PluginId on,
383                                      Map<PluginId, IdeaPluginDescriptor> map,
384                                      final boolean checkModuleDependencies) {
385     for (PluginId id: descriptor.getDependentPluginIds()) {
386       if (!checkModuleDependencies && isModuleDependency(id)) {
387         continue;
388       }
389       if (id.equals(on)) {
390         return true;
391       }
392       final IdeaPluginDescriptor depDescriptor = map.get(id);
393       if (depDescriptor != null && isDependent(depDescriptor, on, map, checkModuleDependencies)) {
394         return true;
395       }
396     }
397     return false;
398   }
399
400   public static boolean isIncompatible(final IdeaPluginDescriptor descriptor) {
401     BuildNumber buildNumber = getBuildNumber();
402
403     if (!StringUtil.isEmpty(descriptor.getSinceBuild())) {
404       BuildNumber sinceBuild = BuildNumber.fromString(descriptor.getSinceBuild());
405       if (sinceBuild.compareTo(buildNumber) > 0) {
406         return true;
407       }
408     }
409
410     if (!StringUtil.isEmpty(descriptor.getUntilBuild()) && !buildNumber.isSnapshot()) {
411       BuildNumber untilBuild = BuildNumber.fromString(descriptor.getUntilBuild());
412       if (untilBuild.compareTo(buildNumber) < 0) return true;
413     }
414
415     return false;
416   }
417
418   private static Comparator<IdeaPluginDescriptor> getPluginDescriptorComparator(Map<PluginId, IdeaPluginDescriptorImpl> idToDescriptorMap) {
419     final Graph<PluginId> graph = createPluginIdGraph(idToDescriptorMap);
420     final DFSTBuilder<PluginId> builder = new DFSTBuilder<PluginId>(graph);
421     /*
422     if (!builder.isAcyclic()) {
423       final Pair<String,String> circularDependency = builder.getCircularDependency();
424       throw new Exception("Cyclic dependencies between plugins are not allowed: \"" + circularDependency.getFirst() + "\" and \"" + circularDependency.getSecond() + "");
425     }
426     */
427     final Comparator<PluginId> idComparator = builder.comparator();
428     return new Comparator<IdeaPluginDescriptor>() {
429       public int compare(IdeaPluginDescriptor o1, IdeaPluginDescriptor o2) {
430         return idComparator.compare(o1.getPluginId(), o2.getPluginId());
431       }
432     };
433   }
434
435   private static Graph<PluginId> createPluginIdGraph(final Map<PluginId, IdeaPluginDescriptorImpl> idToDescriptorMap) {
436     final PluginId[] ids = idToDescriptorMap.keySet().toArray(new PluginId[idToDescriptorMap.size()]);
437     return GraphGenerator.create(CachingSemiGraph.create(new GraphGenerator.SemiGraph<PluginId>() {
438       public Collection<PluginId> getNodes() {
439         return Arrays.asList(ids);
440       }
441
442       public Iterator<PluginId> getIn(PluginId pluginId) {
443         final IdeaPluginDescriptor descriptor = idToDescriptorMap.get(pluginId);
444         ArrayList<PluginId> plugins = new ArrayList<PluginId>();
445         for(PluginId dependentPluginId: descriptor.getDependentPluginIds()) {
446           // check for missing optional dependency
447           if (idToDescriptorMap.containsKey(dependentPluginId)) {
448             plugins.add(dependentPluginId);
449           }
450         }
451         return plugins.iterator();
452       }
453     }));
454   }
455
456   private static ClassLoader[] getParentLoaders(Map<PluginId, IdeaPluginDescriptorImpl> idToDescriptorMap, PluginId[] pluginIds) {
457     if (isUnitTestMode()) return new ClassLoader[0];
458     final List<ClassLoader> classLoaders = new ArrayList<ClassLoader>();
459     for (final PluginId id : pluginIds) {
460       IdeaPluginDescriptor pluginDescriptor = idToDescriptorMap.get(id);
461       if (pluginDescriptor == null) {
462         continue; // Might be an optional dependency
463       }
464
465       final ClassLoader loader = pluginDescriptor.getPluginClassLoader();
466       if (loader == null) {
467         getLogger().assertTrue(false, "Plugin class loader should be initialized for plugin " + id);
468       }
469       classLoaders.add(loader);
470     }
471     return classLoaders.toArray(new ClassLoader[classLoaders.size()]);
472   }
473
474   public static IdeaPluginDescriptorImpl[] loadDescriptors() {
475     if (ClassloaderUtil.isLoadingOfExternalPluginsDisabled()) {
476       return IdeaPluginDescriptorImpl.EMPTY_ARRAY;
477     }
478
479     final List<IdeaPluginDescriptorImpl> result = new ArrayList<IdeaPluginDescriptorImpl>();
480
481     loadDescriptors(PathManager.getPluginsPath(), result);
482     Application application = ApplicationManager.getApplication();
483     if (application == null || !application.isUnitTestMode()) {
484       loadDescriptors(PathManager.getPreinstalledPluginsPath(), result);
485     }
486
487     loadDescriptorsFromProperty(result);
488
489     loadDescriptorsFromClassPath(result);
490
491     IdeaPluginDescriptorImpl[] pluginDescriptors = result.toArray(new IdeaPluginDescriptorImpl[result.size()]);
492     try {
493       Arrays.sort(pluginDescriptors, new PluginDescriptorComparator(pluginDescriptors));
494     }
495     catch (Exception e) {
496       prepareLoadingPluginsErrorMessage(IdeBundle.message("error.plugins.were.not.loaded", e.getMessage()));
497       getLogger().info(e);
498       pluginDescriptors = IdeaPluginDescriptorImpl.EMPTY_ARRAY;
499     }
500     return pluginDescriptors;
501   }
502
503   public static void reportPluginError() {
504     if (myPluginError != null) {
505       JOptionPane.showMessageDialog(null, myPluginError, IdeBundle.message("title.plugin.error"), JOptionPane.ERROR_MESSAGE);
506       myPluginError = null;
507     }
508   }
509
510   private static void loadDescriptorsFromProperty(final List<IdeaPluginDescriptorImpl> result) {
511     final String pathProperty = System.getProperty(PROPERTY_PLUGIN_PATH);
512     if (pathProperty == null) return;
513
514     for (java.util.StringTokenizer t = new java.util.StringTokenizer(pathProperty, File.pathSeparator); t.hasMoreTokens();) {
515       String s = t.nextToken();
516       final IdeaPluginDescriptorImpl ideaPluginDescriptor = loadDescriptor(new File(s), PLUGIN_XML);
517       if (ideaPluginDescriptor != null) {
518         result.add(ideaPluginDescriptor);
519       }
520     }
521   }
522
523   @SuppressWarnings({"UseOfSystemOutOrSystemErr", "CallToPrintStackTrace"})
524   private static void loadDescriptorsFromClassPath(final List<IdeaPluginDescriptorImpl> result) {
525     try {
526       final Collection<URL> urls = getClassLoaderUrls();
527       for (URL url : urls) {
528         final String protocol = url.getProtocol();
529         if ("file".equals(protocol)) {
530           final File file = new File(URLDecoder.decode(url.getFile()));
531           //final String canonicalPath = file.getCanonicalPath();
532           //if (!canonicalPath.startsWith(homePath) || canonicalPath.endsWith(".jar")) continue;
533           //if (!canonicalPath.startsWith(homePath)) continue;
534
535           final String platformPrefix = System.getProperty("idea.platform.prefix");
536           IdeaPluginDescriptorImpl platformPluginDescriptor = null;
537           if (platformPrefix != null) {
538             platformPluginDescriptor = loadDescriptor(file, platformPrefix + "Plugin.xml");
539             if (platformPluginDescriptor != null && !result.contains(platformPluginDescriptor)) {
540               platformPluginDescriptor.setUseCoreClassLoader(true);
541               result.add(platformPluginDescriptor);
542             }
543           }
544
545           IdeaPluginDescriptorImpl pluginDescriptor = loadDescriptor(file, PLUGIN_XML);
546           if (platformPrefix != null && pluginDescriptor != null && pluginDescriptor.getName().equals("IDEA CORE")) {
547             continue;
548           }
549           if (pluginDescriptor != null && !result.contains(pluginDescriptor)) {
550             if (platformPluginDescriptor != null) {
551               // if we found a regular plugin.xml in the same .jar/root as a platform-prefixed descriptor, use the core loader for it too
552               pluginDescriptor.setUseCoreClassLoader(true);
553             }
554             result.add(pluginDescriptor);
555           }
556         }
557       }
558     }
559     catch (Exception e) {
560       System.err.println("Error loading plugins from classpath:");
561       e.printStackTrace();
562     }
563   }
564
565   @SuppressWarnings({"EmptyCatchBlock"})
566   private static Collection<URL> getClassLoaderUrls() {
567     final ClassLoader classLoader = PluginManager.class.getClassLoader();
568     final Class<? extends ClassLoader> aClass = classLoader.getClass();
569     if (aClass.getName().equals(UrlClassLoader.class.getName())) {
570       try {
571         return (List<URL>)aClass.getDeclaredMethod("getUrls").invoke(classLoader);
572       }
573       catch (IllegalAccessException e) {
574       }
575       catch (InvocationTargetException e) {
576       }
577       catch (NoSuchMethodException e) {
578       }
579     }
580     if (classLoader instanceof URLClassLoader) {
581       return Arrays.asList(((URLClassLoader)classLoader).getURLs());
582     }
583
584     return Collections.emptyList();
585   }
586
587   @Nullable
588   private static String filterBadPlugins(List<IdeaPluginDescriptorImpl> result) {
589     final Map<PluginId, IdeaPluginDescriptorImpl> idToDescriptorMap = new HashMap<PluginId, IdeaPluginDescriptorImpl>();
590     final StringBuffer message = new StringBuffer();
591     boolean pluginsWithoutIdFound = false;
592     for (Iterator<IdeaPluginDescriptorImpl> it = result.iterator(); it.hasNext();) {
593       final IdeaPluginDescriptorImpl descriptor = it.next();
594       final PluginId id = descriptor.getPluginId();
595       if (id == null) {
596         pluginsWithoutIdFound = true;
597       }
598       if (idToDescriptorMap.containsKey(id)) {
599         if (message.length() > 0) {
600           message.append("\n");
601         }
602         message.append(IdeBundle.message("message.duplicate.plugin.id"));
603         message.append(id);
604         it.remove();
605       }
606       else if (descriptor.isEnabled()) {
607         idToDescriptorMap.put(id, descriptor);
608       }
609     }
610     addModulesAsDependents(idToDescriptorMap);
611     final List<String> disabledPluginIds = new ArrayList<String>();
612     for (final Iterator<IdeaPluginDescriptorImpl> it = result.iterator(); it.hasNext();) {
613       final IdeaPluginDescriptorImpl pluginDescriptor = it.next();
614       checkDependants(pluginDescriptor, new Function<PluginId, IdeaPluginDescriptor>() {
615         public IdeaPluginDescriptor fun(final PluginId pluginId) {
616           return idToDescriptorMap.get(pluginId);
617         }
618       }, new Condition<PluginId>() {
619         public boolean value(final PluginId pluginId) {
620           if (!idToDescriptorMap.containsKey(pluginId)) {
621             if (message.length() > 0) {
622               message.append("\n");
623             }
624             pluginDescriptor.setEnabled(false);
625             disabledPluginIds.add(pluginDescriptor.getPluginId().getIdString());
626             message.append(getDisabledPlugins().contains(pluginId.getIdString())
627                            ? IdeBundle.message("error.required.plugin.disabled", pluginDescriptor.getPluginId(), pluginId)
628                            : IdeBundle.message("error.required.plugin.not.installed", pluginDescriptor.getPluginId(), pluginId));
629             it.remove();
630             return false;
631           }
632           return true;
633         }
634       });
635     }
636     if (!disabledPluginIds.isEmpty()) {
637       try {
638         saveDisabledPlugins(disabledPluginIds, true);
639       }
640       catch (IOException e) {
641         getLogger().error(e);
642       }
643     }
644     if (pluginsWithoutIdFound) {
645       if (message.length() > 0) {
646         message.append("\n");
647       }
648       message.append(IdeBundle.message("error.plugins.without.id.found"));
649     }
650     if (message.length() > 0) {
651       message.insert(0, IdeBundle.message("error.problems.found.loading.plugins"));
652       return message.toString();
653     }
654     return null;
655   }
656
657   public static void checkDependants(final IdeaPluginDescriptor pluginDescriptor,
658                                      final Function<PluginId, IdeaPluginDescriptor> pluginId2Descriptor,
659                                      final Condition<PluginId> check) {
660     checkDependants(pluginDescriptor, pluginId2Descriptor, check, new HashSet<PluginId>());
661   }
662
663   private static boolean checkDependants(final IdeaPluginDescriptor pluginDescriptor,
664                                          final Function<PluginId, IdeaPluginDescriptor> pluginId2Descriptor,
665                                          final Condition<PluginId> check,
666                                          final Set<PluginId> processed) {
667     processed.add(pluginDescriptor.getPluginId());
668     final PluginId[] dependentPluginIds = pluginDescriptor.getDependentPluginIds();
669     final Set<PluginId> optionalDependencies = new HashSet<PluginId>(Arrays.asList(pluginDescriptor.getOptionalDependentPluginIds()));
670     for (final PluginId dependentPluginId : dependentPluginIds) {
671       if (processed.contains(dependentPluginId)) continue;
672
673       // TODO[yole] should this condition be a parameter?
674       if (isModuleDependency(dependentPluginId) && (ourAvailableModules.isEmpty() || ourAvailableModules.contains(dependentPluginId.getIdString()))) {  
675         continue;
676       }
677       if (!optionalDependencies.contains(dependentPluginId)) {
678         if (!check.value(dependentPluginId)) {
679           return false;
680         }
681         final IdeaPluginDescriptor dependantPluginDescriptor = pluginId2Descriptor.fun(dependentPluginId);
682         if (dependantPluginDescriptor != null && !checkDependants(dependantPluginDescriptor, pluginId2Descriptor, check, processed)) {
683           return false;
684         }
685       }
686     }
687     return true;
688   }
689
690   static BuildNumber getBuildNumber() {
691     if (ourBuildNumber == null) {
692       ourBuildNumber = BuildNumber.fromString(System.getProperty("idea.plugins.compatible.build"));
693       if (ourBuildNumber == null) {
694         try {
695           File buildTxtFile =
696             FileUtil.findFirstThatExist(PathManager.getHomePath() + "/build.txt", PathManager.getHomePath() + "/community/build.txt");
697
698           if (buildTxtFile != null) {
699             ourBuildNumber = BuildNumber.fromString(new String(FileUtil.loadFileText(buildTxtFile)).trim());
700           }
701           else {
702             ourBuildNumber = BuildNumber.fromString("94.SNAPSHOT");
703           }
704         }
705         catch (IOException e) {
706           ourBuildNumber = BuildNumber.fromString("94.SNAPSHOT");
707         }
708       }
709     }
710     return ourBuildNumber;
711   }
712
713
714   private static void loadDescriptors(String pluginsPath, List<IdeaPluginDescriptorImpl> result) {
715     final File pluginsHome = new File(pluginsPath);
716     final File[] files = pluginsHome.listFiles();
717     if (files != null) {
718       for (File file : files) {
719         final IdeaPluginDescriptorImpl descriptor = loadDescriptor(file, PLUGIN_XML);
720         if (descriptor == null) continue;
721         int oldIndex = result.indexOf(descriptor);
722         if (oldIndex >= 0) {
723           final IdeaPluginDescriptorImpl oldDescriptor = result.get(oldIndex);
724           if (StringUtil.compareVersionNumbers(oldDescriptor.getVersion(), descriptor.getVersion()) < 0) {
725             result.set(oldIndex, descriptor);
726           }
727         }
728         else {
729           result.add(descriptor);
730         }
731       }
732     }
733   }
734
735   @SuppressWarnings({"HardCodedStringLiteral"})
736   @Nullable
737   public static IdeaPluginDescriptorImpl loadDescriptor(final File file, final @NonNls String fileName) {
738     IdeaPluginDescriptorImpl descriptor = null;
739
740     if (file.isDirectory()) {
741       descriptor = loadDescriptorFromDir(file, fileName);
742
743       if (descriptor == null) {
744        File libDir = new File(file, "lib");
745        if (!libDir.isDirectory()) {
746          return null;
747        }
748        final File[] files = libDir.listFiles();
749        if (files == null || files.length == 0) {
750          return null;
751        }
752        for (final File f : files) {
753          if (ClassloaderUtil.isJarOrZip(f)) {
754            IdeaPluginDescriptorImpl descriptor1 = loadDescriptorFromJar(f, fileName);
755            if (descriptor1 != null) {
756              if (descriptor != null) {
757                getLogger().info("Cannot load " + file + " because two or more plugin.xml's detected");
758                return null;
759              }
760              descriptor = descriptor1;
761              descriptor.setPath(file);
762            }
763          }
764          else if (f.isDirectory()) {
765            IdeaPluginDescriptorImpl descriptor1 = loadDescriptorFromDir(f, fileName);
766            if (descriptor1 != null) {
767              if (descriptor != null) {
768                getLogger().info("Cannot load " + file + " because two or more plugin.xml's detected");
769                return null;
770              }
771              descriptor = descriptor1;
772              descriptor.setPath(file);
773            }
774          }
775        }
776      }
777     }
778     else if (StringUtil.endsWithIgnoreCase(file.getName(), ".jar")) {
779       descriptor = loadDescriptorFromJar(file, fileName);
780     }
781
782     if (descriptor != null && !descriptor.getOptionalConfigs().isEmpty()) {
783       final Map<PluginId, IdeaPluginDescriptorImpl> descriptors = new HashMap<PluginId, IdeaPluginDescriptorImpl>(descriptor.getOptionalConfigs().size());
784       for (Map.Entry<PluginId, String> entry: descriptor.getOptionalConfigs().entrySet()) {
785         final IdeaPluginDescriptorImpl optionalDescriptor = loadDescriptor(file, entry.getValue());
786         if (optionalDescriptor != null) {
787           descriptors.put(entry.getKey(), optionalDescriptor);
788         }
789       }
790       descriptor.setOptionalDescriptors(descriptors);
791     }
792     return descriptor;
793   }
794
795   @Nullable
796   private static IdeaPluginDescriptorImpl loadDescriptorFromDir(final File file, @NonNls String fileName) {
797     IdeaPluginDescriptorImpl descriptor = null;
798     File descriptorFile = new File(file, META_INF + File.separator + fileName);
799     if (descriptorFile.exists()) {
800       descriptor = new IdeaPluginDescriptorImpl(file);
801
802       try {
803         descriptor.readExternal(descriptorFile.toURL());
804       }
805       catch (Exception e) {
806         System.err.println("Cannot load: " + descriptorFile.getAbsolutePath());
807         e.printStackTrace();
808       }
809     }
810     return descriptor;
811   }
812
813   @Nullable
814   public static IdeaPluginDescriptorImpl loadDescriptorFromJar(File file) {
815     return loadDescriptorFromJar(file, PLUGIN_XML);
816   }
817
818   @Nullable
819   private static IdeaPluginDescriptorImpl loadDescriptorFromJar(File file, @NonNls String fileName) {
820     try {
821
822       IdeaPluginDescriptorImpl descriptor = new IdeaPluginDescriptorImpl(file);
823
824       URI fileURL = file.toURI();
825       URL jarURL = new URL(
826         "jar:" + StringUtil.replace(fileURL.toASCIIString(), "!", "%21") + "!/META-INF/" + fileName
827       );
828
829       descriptor.readExternal(jarURL);
830       return descriptor;
831     }
832     catch (XmlSerializationException e) {
833       getLogger().info("Cannot load " + file, e);
834       prepareLoadingPluginsErrorMessage("Plugin file " + file.getName() + " contains invalid plugin descriptor file.");
835     }
836     catch (FileNotFoundException e) {
837       return null;
838     }
839     catch (Exception e) {
840       getLogger().info("Cannot load " + file, e);
841     }
842
843     return null;
844   }
845
846   @Nullable
847   private static ClassLoader createPluginClassLoader(final File[] classPath,
848                                                          final ClassLoader[] parentLoaders,
849                                                          IdeaPluginDescriptor pluginDescriptor) {
850
851     if (pluginDescriptor.getUseIdeaClassLoader()) {
852       try {
853         final ClassLoader loader = PluginManager.class.getClassLoader();
854         final Method addUrlMethod = getAddUrlMethod(loader);
855
856
857         for (File aClassPath : classPath) {
858           final File file = aClassPath.getCanonicalFile();
859           addUrlMethod.invoke(loader,  file.toURL());
860         }
861
862         return loader;
863       }
864       catch (NoSuchMethodException e) {
865         e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
866       }
867       catch (IOException e) {
868         e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
869       }
870       catch (IllegalAccessException e) {
871         e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
872       }
873       catch (InvocationTargetException e) {
874         e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
875       }
876     }
877
878     PluginId pluginId = pluginDescriptor.getPluginId();
879     File pluginRoot = pluginDescriptor.getPath();
880
881     if (isUnitTestMode()) return null;
882     try {
883       final List<URL> urls = new ArrayList<URL>(classPath.length);
884       for (File aClassPath : classPath) {
885         final File file = aClassPath.getCanonicalFile(); // it is critical not to have "." and ".." in classpath elements
886         urls.add(file.toURL());
887       }
888       return new PluginClassLoader(urls, parentLoaders, pluginId, pluginRoot);
889     }
890     catch (MalformedURLException e) {
891       e.printStackTrace();
892     }
893     catch (IOException e) {
894       e.printStackTrace();
895     }
896     return null;
897   }
898
899   @SuppressWarnings({"HardCodedStringLiteral"})
900   private static Method getAddUrlMethod(final ClassLoader loader) throws NoSuchMethodException {
901     if (loader instanceof URLClassLoader) {
902       final Method addUrlMethod = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
903       addUrlMethod.setAccessible(true);
904       return addUrlMethod;
905     }
906
907     return loader.getClass().getDeclaredMethod("addURL", URL.class);
908   }
909
910
911   public static boolean isPluginInstalled(PluginId id) {
912     return (getPlugin(id) != null);
913   }
914
915   @Nullable
916   public static IdeaPluginDescriptor getPlugin(PluginId id) {
917     final IdeaPluginDescriptor[] plugins = getPlugins();
918     for (final IdeaPluginDescriptor plugin : plugins) {
919       if (Comparing.equal(id, plugin.getPluginId())) {
920         return plugin;
921       }
922     }
923     return null;
924   }
925
926   public static void addPluginClass(String className, PluginId pluginId) {
927     synchronized(PLUGIN_CLASSES_LOCK) {
928       if (ourPluginClasses == null) {
929         ourPluginClasses = new THashMap<String, PluginId>();
930       }
931       ourPluginClasses.put(className, pluginId);
932     }
933   }
934
935   public static boolean isPluginClass(String className) {
936     return getPluginByClassName(className) != null;
937   }
938
939   @Nullable
940   public static PluginId getPluginByClassName(String className) {
941     synchronized (PLUGIN_CLASSES_LOCK) {
942       return ourPluginClasses != null ? ourPluginClasses.get(className) : null;
943     }
944   }
945
946   public static boolean disablePlugin(String id) {
947     if (getDisabledPlugins().contains(id)) return false;
948     getDisabledPlugins().add(id);
949     try {
950       saveDisabledPlugins(getDisabledPlugins(), false);
951     }
952     catch (IOException e) {
953       return false;
954     }
955     return true;
956   }
957
958   public static void saveDisabledPlugins(Collection<String> ids, boolean append) throws IOException {
959     File plugins = new File(PathManager.getConfigPath(), PluginManager.DISABLED_PLUGINS_FILENAME);
960     if (!plugins.isFile()) {
961       FileUtil.ensureCanCreateFile(plugins);
962     }
963     PrintWriter printWriter = null;
964     try {
965       printWriter = new PrintWriter(new BufferedWriter(new FileWriter(plugins, append)));
966       for (String id : ids) {
967         printWriter.println(id);
968       }
969       printWriter.flush();
970     }
971     finally {
972       if (printWriter != null) {
973         printWriter.close();
974       }
975     }
976   }
977
978   public static List<String> getDisabledPlugins() {
979     if (ourDisabledPlugins == null) {
980       ourDisabledPlugins = new ArrayList<String>();
981       if (System.getProperty("idea.ignore.disabled.plugins") == null && !isUnitTestMode()) {
982         loadDisabledPlugins(PathManager.getConfigPath(), ourDisabledPlugins);
983       }
984     }
985     return ourDisabledPlugins;
986   }
987
988   public static void loadDisabledPlugins(final String configPath, final Collection<String> disabledPlugins) {
989     final File file = new File(configPath, DISABLED_PLUGINS_FILENAME);
990     if (file.isFile()) {
991       BufferedReader reader = null;
992       try {
993         reader = new BufferedReader(new FileReader(file));
994         String id;
995         while ((id = reader.readLine()) != null) {
996           disabledPlugins.add(id.trim());
997         }
998       }
999       catch (IOException e) {
1000         //do nothing
1001       }
1002       finally {
1003         try {
1004           if (reader != null) {
1005             reader.close();
1006           }
1007         }
1008         catch (IOException e) {
1009           //do nothing
1010         }
1011       }
1012     }
1013   }
1014
1015   public static void disableIncompatiblePlugin(final Object cause, final Throwable ex) {
1016     final PluginId pluginId = getPluginByClassName(cause.getClass().getName());
1017     if (pluginId != null && !ApplicationManager.getApplication().isHeadlessEnvironment()) {
1018       final boolean success = disablePlugin(pluginId.getIdString());
1019       SwingUtilities.invokeLater(new Runnable() {
1020         public void run() {
1021           JOptionPane.showMessageDialog(JOptionPane.getRootFrame(),
1022                                         "Incompatible plugin detected: " + pluginId.getIdString() +
1023                                            (success ? "\nThe plugin has been disabled" : ""),
1024                                         "Plugin Manager",
1025                                         JOptionPane.ERROR_MESSAGE);
1026         }
1027       });
1028     }
1029     else {
1030       // should never happen
1031       throw new RuntimeException(ex);
1032     }
1033   }
1034
1035   private static boolean isUnitTestMode() {
1036     final Application app = ApplicationManager.getApplication();
1037     return app != null && app.isUnitTestMode();
1038   }
1039
1040   private static class IdeaLogProvider implements LogProvider {
1041     public void error(String message) {
1042       getLogger().error(message);
1043     }
1044
1045     public void error(String message, Throwable t) {
1046       getLogger().error(message, t);
1047     }
1048
1049     public void error(Throwable t) {
1050       getLogger().error(t);
1051     }
1052
1053     public void warn(String message) {
1054       getLogger().info(message);
1055     }
1056
1057     public void warn(String message, Throwable t) {
1058       getLogger().info(message, t);
1059     }
1060
1061     public void warn(Throwable t) {
1062       getLogger().info(t);
1063     }
1064   }
1065
1066   private static class LoggerHolder {
1067     private static final Logger ourLogger = Logger.getInstance("#com.intellij.ide.plugins.PluginManager");
1068   }
1069
1070   public static Logger getLogger() {
1071     return LoggerHolder.ourLogger;
1072   }
1073
1074   private static class ClassCounter {
1075     private String myPluginId;
1076     private int myCount;
1077
1078     private ClassCounter(String pluginId) {
1079       myPluginId = pluginId;
1080       myCount = 1;
1081     }
1082
1083     private void increment() {
1084       myCount++;
1085     }
1086
1087     @Override
1088     public String toString() {
1089       return myPluginId + ": " + myCount;
1090     }
1091   }
1092
1093   public static void dumpPluginClassStatistics() {
1094     if (!Boolean.valueOf(System.getProperty("idea.is.internal")).booleanValue()) return;
1095     Map<String, ClassCounter> pluginToClassMap = new HashMap<String, ClassCounter>();
1096     synchronized (PLUGIN_CLASSES_LOCK) {
1097       for (Map.Entry<String, PluginId> entry : ourPluginClasses.entrySet()) {
1098         String id = entry.getValue().toString();
1099         final ClassCounter counter = pluginToClassMap.get(id);
1100         if (counter != null) {
1101           counter.increment();
1102         }
1103         else {
1104           pluginToClassMap.put(id, new ClassCounter(id));
1105         }
1106       }
1107     }
1108     List<ClassCounter> counters = new ArrayList<ClassCounter>(pluginToClassMap.values());
1109     Collections.sort(counters, new Comparator<ClassCounter>() {
1110       public int compare(ClassCounter o1, ClassCounter o2) {
1111         return o2.myCount - o1.myCount;
1112       }
1113     });
1114     for (ClassCounter counter : counters) {
1115       getLogger().info(counter.toString());
1116     }
1117   }
1118 }