001/*
002 * Sonar, open source software quality management tool.
003 * Copyright (C) 2008-2012 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 */
020package org.sonar.core.plugins;
021
022import com.google.common.collect.Lists;
023import com.google.common.collect.Maps;
024import org.apache.commons.lang.StringUtils;
025import org.codehaus.plexus.classworlds.ClassWorld;
026import org.codehaus.plexus.classworlds.realm.ClassRealm;
027import org.codehaus.plexus.classworlds.realm.NoSuchRealmException;
028import org.slf4j.Logger;
029import org.slf4j.LoggerFactory;
030import org.sonar.api.Plugin;
031import org.sonar.api.platform.PluginMetadata;
032import org.sonar.api.utils.SonarException;
033
034import java.io.File;
035import java.net.URL;
036import java.util.Collection;
037import java.util.List;
038import 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 */
058public 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}