View Javadoc

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 }