1 /*
2 * ModeShape (http://www.modeshape.org)
3 * See the COPYRIGHT.txt file distributed with this work for information
4 * regarding copyright ownership. Some portions may be licensed
5 * to Red Hat, Inc. under one or more contributor license agreements.
6 * See the AUTHORS.txt file in the distribution for a full listing of
7 * individual contributors.
8 *
9 * ModeShape is free software. Unless otherwise indicated, all code in ModeShape
10 * is licensed to you under the terms of the GNU Lesser General Public License as
11 * published by the Free Software Foundation; either version 2.1 of
12 * the License, or (at your option) any later version.
13 *
14 * ModeShape is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17 * Lesser General Public License for more details.
18 *
19 * You should have received a copy of the GNU Lesser General Public
20 * License along with this software; if not, write to the Free
21 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
22 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
23 */
24 package org.modeshape.maven;
25
26 import java.net.MalformedURLException;
27 import java.net.URL;
28 import java.net.URLClassLoader;
29 import java.util.ArrayList;
30 import java.util.Collections;
31 import java.util.HashMap;
32 import java.util.HashSet;
33 import java.util.LinkedHashMap;
34 import java.util.List;
35 import java.util.Map;
36 import java.util.Set;
37 import java.util.concurrent.locks.Lock;
38 import java.util.concurrent.locks.ReadWriteLock;
39 import java.util.concurrent.locks.ReentrantLock;
40 import java.util.concurrent.locks.ReentrantReadWriteLock;
41 import org.modeshape.common.SystemFailureException;
42
43 /**
44 * A managed collection of {@link ClassLoader class loaders} that access JARs in a JCR repository, processing dependencies
45 * according to <a
46 * href="http://maven.apache.org/guides/introduction/introduction-to-optional-and-excludes-dependencies.html">Maven 2 transitive
47 * dependency rules</a>. Each {@link MavenRepository} instance owns an instance of this class, which provides a cached set of
48 * class loaders and a facility for {@link MavenRepository#getClassLoader(ClassLoader, MavenId...) getting class loaders} based
49 * upon a set of one or more versioned libraries.
50 */
51 /* package */class MavenClassLoaders {
52
53 private final MavenRepository repository;
54 private final Lock lock = new ReentrantLock();
55
56 /**
57 * The class loaders, keyed by Maven project ID, that are responsible for the project's classpath. Basically, each project
58 * class loader delegates first to the JAR file class loader, and if not found, delegates to the project class loaders for
59 * each of the project's dependencies.
60 * <p>
61 * These class loaders are loaded lazily and are never removed from the map, but may be changed as dependencies of the project
62 * are updated.
63 * </p>
64 */
65 private final Map<MavenId, ProjectClassLoader> projectClassLoaders = new HashMap<MavenId, ProjectClassLoader>();
66
67 /**
68 * Create with a specified repository and optionally the parent class loader that should be consulted first and a default
69 * class loader that should be consulted after all others.
70 *
71 * @param repository the Maven repository; may not be null
72 */
73 /* package */MavenClassLoaders( MavenRepository repository ) {
74 this.repository = repository;
75 }
76
77 protected ProjectClassLoader getProjectClassLoader( MavenId mavenId ) {
78 ProjectClassLoader result = null;
79 try {
80 this.lock.lock();
81 result = this.projectClassLoaders.get(mavenId);
82 if (result == null) {
83 // The project has not yet been loaded, so get URL to the JAR file and get the dependencies ...
84 URL jarFileUrl = this.repository.getUrl(mavenId, ArtifactType.JAR, null);
85 URLClassLoader jarFileLoader = new URLClassLoader(new URL[] {jarFileUrl}, null);
86
87 List<MavenDependency> dependencies = this.repository.getDependencies(mavenId);
88 result = new ProjectClassLoader(mavenId, jarFileLoader);
89 result.setDependencies(dependencies);
90 this.projectClassLoaders.put(mavenId, result);
91 }
92 } catch (MalformedURLException e) {
93 // This really should never happen, but if it does ...
94 throw new SystemFailureException(MavenI18n.errorGettingUrlForMavenProject.text(mavenId), e);
95 } finally {
96 this.lock.unlock();
97 }
98 return result;
99 }
100
101 public ProjectClassLoader getClassLoader( ClassLoader parent,
102 MavenId... mavenIds ) {
103 if (parent == null) parent = Thread.currentThread().getContextClassLoader();
104 if (parent == null) parent = this.getClass().getClassLoader();
105 ProjectClassLoader result = new ProjectClassLoader(parent);
106 // Create a dependencies list for the desired projects ...
107 List<MavenDependency> dependencies = new ArrayList<MavenDependency>();
108 for (MavenId mavenId : mavenIds) {
109 if (!dependencies.contains(mavenId)) {
110 MavenDependency dependency = new MavenDependency(mavenId);
111 dependencies.add(dependency);
112 }
113 }
114 result.setDependencies(dependencies);
115 return result;
116 }
117
118 public void notifyChangeInDependencies( MavenId mavenId ) {
119 List<MavenDependency> dependencies = this.repository.getDependencies(mavenId);
120 try {
121 this.lock.lock();
122 ProjectClassLoader existingLoader = this.projectClassLoaders.get(mavenId);
123 if (existingLoader != null) {
124 existingLoader.setDependencies(dependencies);
125 }
126 } finally {
127 this.lock.unlock();
128 }
129 }
130
131 /**
132 * A project class loader is responsible for loading all classes and resources for the project, including delegating to
133 * dependent projects if required and adhearing to all stated exclusions.
134 *
135 * @author Randall Hauch
136 */
137 protected class ProjectClassLoader extends ClassLoader {
138
139 private final MavenId mavenId;
140 private final URLClassLoader jarFileClassLoader;
141 private final Map<MavenId, ProjectClassLoader> dependencies = new LinkedHashMap<MavenId, ProjectClassLoader>();
142 private final Map<MavenId, Set<MavenId>> exclusions = new HashMap<MavenId, Set<MavenId>>();
143 private final ReadWriteLock dependencyLock = new ReentrantReadWriteLock();
144
145 /**
146 * Create a class loader for the given project.
147 *
148 * @param mavenId
149 * @param jarFileClassLoader
150 * @see MavenClassLoaders#getClassLoader(ClassLoader, MavenId...)
151 */
152 protected ProjectClassLoader( MavenId mavenId,
153 URLClassLoader jarFileClassLoader ) {
154 super(null);
155 this.mavenId = mavenId;
156 this.jarFileClassLoader = jarFileClassLoader;
157 }
158
159 /**
160 * Create a class loader that doesn't reference a top level project, but instead references multiple projects.
161 *
162 * @param parent
163 * @see MavenClassLoaders#getClassLoader(ClassLoader, MavenId...)
164 */
165 protected ProjectClassLoader( ClassLoader parent ) {
166 super(parent);
167 this.mavenId = null;
168 this.jarFileClassLoader = null;
169 }
170
171 protected void setDependencies( List<MavenDependency> dependencies ) {
172 try {
173 this.dependencyLock.writeLock().lock();
174 this.dependencies.clear();
175 if (dependencies != null) {
176 // Find all of the project class loaders for the dependent projects ...
177 for (MavenDependency dependency : dependencies) {
178 ProjectClassLoader dependencyClassLoader = MavenClassLoaders.this.getProjectClassLoader(dependency.getId());
179 if (dependencyClassLoader != null) {
180 MavenId dependencyId = dependency.getId();
181 this.dependencies.put(dependencyId, dependencyClassLoader);
182 this.exclusions.put(dependencyId, Collections.unmodifiableSet(dependency.getExclusions()));
183 }
184 }
185 }
186 } finally {
187 this.dependencyLock.writeLock().unlock();
188 }
189 }
190
191 /**
192 * Finds the resource with the given name. This implementation first consults the class loader for this project's JAR
193 * file, and failing that, consults all of the class loaders for the dependent projects.
194 *
195 * @param name The resource name
196 * @return A <tt>URL</tt> object for reading the resource, or <tt>null</tt> if the resource could not be found or the
197 * invoker doesn't have adequate privileges to get the resource.
198 */
199 @Override
200 protected URL findResource( String name ) {
201 // This method is called only by the top-level class loader handling a request.
202 return findResource(name, null);
203 }
204
205 /**
206 * This method is called by the top-level class loader {@link #findResource(String) when finding a resource}. <i>In fact,
207 * this method should only be directly called by test methods; subclasses should call {@link #findResource(String)}.</i>
208 * <p>
209 * This method first looks in this project's JAR. If the resource is not found, then this method
210 * {@link #findResource(String, Set, Set, List) searches the dependencies}. This method's signature allows for a list to
211 * be supplied for reporting the list of projects that make up the searched classpath (excluding those that were never
212 * searched because the resource was found).
213 * </p>
214 *
215 * @param name The resource name
216 * @param debugSearchPath the list into which the IDs of the searched projects will be placed; may be null if this
217 * information is not needed
218 * @return A <tt>URL</tt> object for reading the resource, or <tt>null</tt> if the resource could not be found or the
219 * invoker doesn't have adequate privileges to get the resource.
220 */
221 protected URL findResource( String name,
222 List<MavenId> debugSearchPath ) {
223 Set<MavenId> processed = new HashSet<MavenId>();
224 // This method is called only by the top-level class loader handling a request.
225 // Therefore, first look in this project's JAR file ...
226 URL result = null;
227 if (this.jarFileClassLoader != null) {
228 result = this.jarFileClassLoader.getResource(name);
229 processed.add(this.mavenId);
230 }
231 if (debugSearchPath != null && this.mavenId != null) debugSearchPath.add(this.mavenId);
232
233 if (result == null) {
234 // Look in the dependencies ...
235 result = findResource(name, processed, null, debugSearchPath);
236 }
237 return result;
238 }
239
240 protected URL findResource( String name,
241 Set<MavenId> processed,
242 Set<MavenId> exclusions,
243 List<MavenId> debugSearchPath ) {
244 // If this project is to be excluded, then simply return ...
245 if (exclusions != null && exclusions.contains(this.mavenId)) return null;
246
247 // Check the class loaders for the dependencies.
248 URL result = null;
249 try {
250 this.dependencyLock.readLock().lock();
251 // First, look in the immediate dependencies ...
252 for (Map.Entry<MavenId, ProjectClassLoader> entry : this.dependencies.entrySet()) {
253 ProjectClassLoader loader = entry.getValue();
254 MavenId id = loader.mavenId;
255 if (processed.contains(id)) continue;
256 if (exclusions != null && exclusions.contains(id)) continue;
257 result = loader.jarFileClassLoader.findResource(name);
258 processed.add(id);
259 if (debugSearchPath != null && id != null) debugSearchPath.add(id);
260 if (result != null) break;
261 }
262 if (result == null) {
263 // Still not found, so look in the dependencies of the immediate dependencies ...
264 for (Map.Entry<MavenId, ProjectClassLoader> entry : this.dependencies.entrySet()) {
265 MavenId dependency = entry.getKey();
266 ProjectClassLoader loader = entry.getValue();
267 // Get the exclusions for this dependency ...
268 Set<MavenId> dependencyExclusions = this.exclusions.get(dependency);
269 if (!dependencyExclusions.isEmpty()) {
270 // Create a new set of exclusions for this branch ...
271 if (exclusions == null) {
272 exclusions = new HashSet<MavenId>();
273 } else {
274 exclusions = new HashSet<MavenId>(exclusions);
275 }
276 // Then add this dependencies exclusion to the set ...
277 exclusions.addAll(dependencyExclusions);
278 }
279 result = loader.findResource(name, processed, exclusions, debugSearchPath);
280 if (result != null) break;
281 }
282 }
283 } finally {
284 this.dependencyLock.readLock().unlock();
285 }
286 return super.findResource(name);
287 }
288 }
289 }