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 */
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 }