001    /*
002     * Sonar, open source software quality management tool.
003     * Copyright (C) 2008-2011 SonarSource
004     * mailto:contact AT sonarsource DOT com
005     *
006     * Sonar is free software; you can redistribute it and/or
007     * modify it under the terms of the GNU Lesser General Public
008     * License as published by the Free Software Foundation; either
009     * version 3 of the License, or (at your option) any later version.
010     *
011     * Sonar is distributed in the hope that it will be useful,
012     * but WITHOUT ANY WARRANTY; without even the implied warranty of
013     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014     * Lesser General Public License for more details.
015     *
016     * You should have received a copy of the GNU Lesser General Public
017     * License along with Sonar; if not, write to the Free Software
018     * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
019     */
020    package org.sonar.core.plugins;
021    
022    import com.google.common.collect.Lists;
023    import com.google.common.collect.Maps;
024    import org.apache.commons.lang.StringUtils;
025    import org.codehaus.plexus.classworlds.ClassWorld;
026    import org.codehaus.plexus.classworlds.realm.ClassRealm;
027    import org.codehaus.plexus.classworlds.realm.NoSuchRealmException;
028    import org.slf4j.Logger;
029    import org.slf4j.LoggerFactory;
030    import org.sonar.api.Plugin;
031    import org.sonar.api.platform.PluginMetadata;
032    import org.sonar.api.utils.SonarException;
033    
034    import java.io.File;
035    import java.net.URL;
036    import java.util.Collection;
037    import java.util.List;
038    import java.util.Map;
039    
040    /**
041     * Encapsulates manipulations with ClassLoaders, such as creation and establishing dependencies. Current implementation based on
042     * {@link ClassWorld}.
043     * <p/>
044     * <h3>IMPORTANT</h3>
045     * <p>
046     * If we have pluginA , then all classes and resources from package and subpackages of <b>org.sonar.plugins.pluginA.api</b> will be visible
047     * for all other plugins even if they located in dependent library.
048     * </p>
049     * <p/>
050     * <h4>Search order for {@link ClassRealm} :</h4>
051     * <ul>
052     * <li>parent class loader (passed via the constructor) if there is one</li>
053     * <li>imports</li>
054     * <li>realm's constituents</li>
055     * <li>parent realm</li>
056     * </ul>
057     */
058    public class PluginClassloaders {
059    
060      private static final String[] PREFIXES_TO_EXPORT = {"org.sonar.plugins.", "com.sonar.plugins.", "com.sonarsource.plugins."};
061      private static final Logger LOG = LoggerFactory.getLogger(PluginClassloaders.class);
062    
063      private ClassWorld world = new ClassWorld();
064      private ClassLoader baseClassloader;
065      private boolean done = false;
066    
067      public PluginClassloaders(ClassLoader baseClassloader) {
068        this.baseClassloader = baseClassloader;
069      }
070    
071      public Map<String, Plugin> init(Collection<PluginMetadata> plugins) {
072        List<PluginMetadata> children = Lists.newArrayList();
073        for (PluginMetadata plugin : plugins) {
074          if (StringUtils.isBlank(plugin.getBasePlugin())) {
075            add(plugin);
076          } else {
077            children.add(plugin);
078          }
079        }
080    
081        for (PluginMetadata child : children) {
082          extend(child);
083        }
084    
085        done();
086    
087        Map<String, Plugin> pluginsByKey = Maps.newHashMap();
088        for (PluginMetadata metadata : plugins) {
089          pluginsByKey.put(metadata.getKey(), instantiatePlugin(metadata));
090        }
091        return pluginsByKey;
092      }
093    
094      public ClassLoader add(PluginMetadata plugin) {
095        if (done) {
096          throw new IllegalStateException("Plugin classloaders are already initialized");
097        }
098        try {
099          List<URL> resources = Lists.newArrayList();
100          List<URL> others = Lists.newArrayList();
101          for (File file : plugin.getDeployedFiles()) {
102            if (isResource(file)) {
103              resources.add(file.toURI().toURL());
104            } else {
105              others.add(file.toURI().toURL());
106            }
107          }
108          ClassLoader parent;
109          if (resources.isEmpty()) {
110            parent = baseClassloader;
111          } else {
112            parent = new ResourcesClassloader(resources, baseClassloader);
113          }
114          final ClassRealm realm;
115          if (plugin.isUseChildFirstClassLoader()) {
116            ClassRealm parentRealm = world.newRealm(plugin.getKey() + "-parent", parent);
117            realm = parentRealm.createChildRealm(plugin.getKey());
118          } else {
119            realm = world.newRealm(plugin.getKey(), parent);
120          }
121          for (URL url : others) {
122            realm.addURL(url);
123          }
124          return realm;
125        } catch (Exception e) {
126          throw new SonarException(e);
127        }
128      }
129    
130      public boolean extend(PluginMetadata plugin) {
131        if (done) {
132          throw new IllegalStateException("Plugin classloaders are already initialized");
133        }
134        try {
135          ClassRealm base = world.getRealm(plugin.getBasePlugin());
136          if (base == null) {
137            // Ignored, because base plugin is not installed
138            LOG.warn("Plugin " + plugin.getKey() + " is ignored because base plugin is not installed: " + plugin.getBasePlugin());
139            return false;
140          }
141          base.createChildRealm(plugin.getKey()); // we create new realm to be able to return it by key without conversion to baseKey
142          for (File file : plugin.getDeployedFiles()) {
143            base.addURL(file.toURI().toURL());
144          }
145          return true;
146        } catch (Exception e) {
147          throw new SonarException(e);
148        }
149      }
150    
151      /**
152       * Establishes dependencies among ClassLoaders.
153       */
154      public void done() {
155        if (done) {
156          throw new IllegalStateException("Plugin classloaders are already initialized");
157        }
158        for (Object o : world.getRealms()) {
159          ClassRealm realm = (ClassRealm) o;
160          if (!StringUtils.endsWith(realm.getId(), "-parent")) {
161            String[] packagesToExport = new String[PREFIXES_TO_EXPORT.length];
162            for (int i = 0; i < PREFIXES_TO_EXPORT.length; i++) {
163              // important to have dot at the end of package name only for classworlds 1.1
164              packagesToExport[i] = PREFIXES_TO_EXPORT[i] + realm.getId() + ".api";
165            }
166            export(realm, packagesToExport);
167          }
168        }
169        done = true;
170      }
171    
172      /**
173       * Exports specified packages from given ClassRealm to all others.
174       */
175      private void export(ClassRealm realm, String... packages) {
176        for (Object o : world.getRealms()) {
177          ClassRealm dep = (ClassRealm) o;
178          if (!StringUtils.equals(dep.getId(), realm.getId())) {
179            try {
180              for (String packageName : packages) {
181                dep.importFrom(realm.getId(), packageName);
182              }
183            } catch (NoSuchRealmException e) {
184              // should never happen
185              throw new SonarException(e);
186            }
187          }
188        }
189      }
190    
191      /**
192       * Note that this method should be called only after creation of all ClassLoaders - see {@link #done()}.
193       */
194      public ClassLoader get(String key) {
195        if (!done) {
196          throw new IllegalStateException("Plugin classloaders are not initialized");
197        }
198        try {
199          return world.getRealm(key);
200        } catch (NoSuchRealmException e) {
201          return null;
202        }
203      }
204    
205      public Plugin instantiatePlugin(PluginMetadata metadata) {
206        try {
207          Class claz = get(metadata.getKey()).loadClass(metadata.getMainClass());
208          return (Plugin) claz.newInstance();
209    
210        } catch (Exception e) {
211          throw new SonarException("Fail to load plugin " + metadata.getKey(), e);
212        }
213      }
214    
215      private boolean isResource(File file) {
216        return !StringUtils.endsWithIgnoreCase(file.getName(), ".jar") && !file.isDirectory();
217      }
218    
219      public void clean() {
220        for (ClassRealm realm : (Collection<ClassRealm>) world.getRealms()) {
221          try {
222            world.disposeRealm(realm.getId());
223          } catch (Exception e) {
224            // Ignore
225          }
226          world=null;
227        }
228      }
229    }