View Javadoc

1   /*
2    * Copyright 2010 - org.tinyjee.maven
3    *
4    *    Licensed under the Apache License, Version 2.0 (the "License");
5    *    you may not use this file except in compliance with the License.
6    *    You may obtain a copy of the License at
7    *
8    *        http://www.apache.org/licenses/LICENSE-2.0
9    *
10   *    Unless required by applicable law or agreed to in writing, software
11   *    distributed under the License is distributed on an "AS IS" BASIS,
12   *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *    See the License for the specific language governing permissions and
14   *    limitations under the License.
15   */
16  
17  package org.tinyjee.maven.dim.spi;
18  
19  import org.apache.maven.doxia.logging.Log;
20  import org.codehaus.plexus.util.StringUtils;
21  
22  import java.io.File;
23  import java.io.IOException;
24  import java.lang.ref.SoftReference;
25  import java.net.*;
26  import java.util.*;
27  import java.util.regex.Pattern;
28  
29  import static java.lang.String.valueOf;
30  import static java.util.Arrays.asList;
31  
32  /**
33   * A collection of utility methods used to resolve resources.
34   *
35   * @author Juergen_Kellerer, 2010-09-06
36   * @version 1.0
37   */
38  public final class ResourceResolver {
39  
40  	private static String currentClassLoaderKey;
41  	private static SoftReference<URLClassLoader> currentClassLoader = new SoftReference<URLClassLoader>(null);
42  
43  	static final Pattern SPLITTER = Pattern.compile("\\s*[,;]+\\s*", Pattern.MULTILINE);
44  
45  	/**
46  	 * Defines the default search path when looking after includes in the site directory.
47  	 * <p/>
48  	 * Paths beginning with "src/main/java", "src/test/java", "src/main/resources", "src/test/resources",
49  	 * "site/" and "target/" are adjusted with the absolute paths if {@link org.tinyjee.maven.dim.InitializeMacroMojo}
50  	 * was called.
51  	 */
52  	static final String[] DEFAULT_SITE_SOURCE_SEARCH_PATH = SPLITTER.split(System.getProperty(
53  			"org.tinyjee.maven.dim.siteSourceSearchPath", "" +
54  			"src/site, " +
55  			"src/site/resources, " +
56  			"site, " +
57  			"site/resources"));
58  
59  	/**
60  	 * Defines the default search path when looking after sources.
61  	 * <p/>
62  	 * Paths beginning with "src/main/java", "src/test/java", "src/main/resources", "src/test/resources",
63  	 * "site/" and "target/" are adjusted with the absolute paths if {@link org.tinyjee.maven.dim.InitializeMacroMojo}
64  	 * was called.
65  	 */
66  	static final String[] DEFAULT_SOURCE_SEARCH_PATH = SPLITTER.split(System.getProperty(
67  			"org.tinyjee.maven.dim.sourceSearchPath", "" +
68  			"src/main/java, " +
69  			"src/main/resources, " +
70  			"src/test/java, " +
71  			"src/test/resources, " +
72  			"src/main, " +
73  			"src, " +
74  			"target"));
75  
76  	static final String CLASSES_PATH = System.getProperty(
77  			"org.tinyjee.maven.dim.include.classesPath",
78  			"target/classes/");
79  
80  	static final String TEST_CLASSES_PATH = System.getProperty(
81  			"org.tinyjee.maven.dim.include.testClassesPath",
82  			"target/test-classes/");
83  
84  	/**
85  	 * Sets additional search paths that are used to support source definitions like '[artifactId]:/some-path' or
86  	 * '[groupId:artifactId]:/some-path'.
87  	 * <p/>
88  	 * This method is primarily called by {@link org.tinyjee.maven.dim.InitializeMacroMojo} and therefore path resolution
89  	 * depends on the Mojo being executed.
90  	 *
91  	 * @param groupId    the group id of the module to set.
92  	 * @param artifactId the artifact id of the module to set.
93  	 * @param basePath   the base path of the module.
94  	 */
95  	public static void setModulePath(String groupId, String artifactId, File basePath) {
96  		// Note: As the initialization Mojo and the Macro run in different classloaders,
97  		//       we must exchange information through system properties as static fields won't work here.
98  		String absolutePath = basePath.getAbsolutePath();
99  		System.setProperty("dim." + artifactId + ".basedir", absolutePath);
100 		System.setProperty("dim." + groupId + '_' + artifactId + ".basedir", absolutePath);
101 	}
102 
103 	/**
104 	 * Returns a module path for the given module name or 'null' if not known.
105 	 *
106 	 * @param moduleName the name of the module, either "groupId:artifactId" or "artifactId".
107 	 * @return a module path for the given module name or 'null' if not known.
108 	 */
109 	public static String getModulePath(String moduleName) {
110 		return System.getProperty("dim." + moduleName.replace(':', '_') + ".basedir");
111 	}
112 
113 	/**
114 	 * Returns all currently defined module paths.
115 	 *
116 	 * @return all currently defined module paths.
117 	 */
118 	public static Map<String, String> getModulePaths() {
119 		Map<String, String> modulePaths = new TreeMap<String, String>();
120 		for (Object entry : System.getProperties().entrySet()) {
121 			String key = valueOf(((Map.Entry) entry).getKey()), value = valueOf(((Map.Entry) entry).getValue());
122 			if (key.startsWith("dim.") && key.endsWith(".basedir"))
123 				modulePaths.put('[' + key.substring(4, key.length() - 8).replace('_', ':') + ']', value);
124 		}
125 		return modulePaths;
126 	}
127 
128 	static File[] canonicalizePath(File basePath, String path) {
129 		File[] result = new File[1];
130 		if (isAbsolute(path)) {
131 			result[0] = new File(path);
132 		} else {
133 			String base;
134 			try {
135 				base = basePath.getCanonicalPath();
136 			} catch (IOException e) {
137 				base = basePath.getAbsolutePath();
138 			}
139 			result = resolveDefaultProjectPaths("siteDirectory", "src/site", base, path, result);
140 			if (result[0] == null) result = resolveDefaultProjectPaths("sourceDirectory", "src/main/java", base, path, result);
141 			if (result[0] == null) result = resolveDefaultProjectPaths("resourceDirectories", "src/main/resources", base, path, result);
142 			if (result[0] == null) result = resolveDefaultProjectPaths("outputDirectory", "target/classes", base, path, result);
143 			if (result[0] == null) result = resolveDefaultProjectPaths("testSourceDirectory", "src/test/java", base, path, result);
144 			if (result[0] == null) result = resolveDefaultProjectPaths("testResourceDirectories", "src/test/resources", base, path, result);
145 			if (result[0] == null) result = resolveDefaultProjectPaths("testOutputDirectory", "target/test-classes", base, path, result);
146 			if (result[0] == null) result = resolveDefaultProjectPaths("targetDirectory", "target", base, path, result);
147 			if (result[0] == null) result[0] = new File(basePath, path);
148 		}
149 
150 		for (int i = 0; i < result.length; i++) {
151 			try {
152 				result[i] = result[i].getCanonicalFile();
153 			} catch (IOException e) {
154 				final Log log = Globals.getLog();
155 				log.warn("Failed to retrieve the canonical path of " + result[i] + '.');
156 				if (log.isDebugEnabled()) log.debug(e.getMessage(), e);
157 			}
158 		}
159 		return result;
160 	}
161 
162 	static File[] resolveDefaultProjectPaths(String projectPathKey, String projectPathPrefix,
163 	                                         String basePath, String path, File[] result) {
164 		path = path.replace('\\', '/');
165 		if (path.equals(projectPathPrefix) || path.startsWith(projectPathPrefix + '/')) {
166 			final String projectPath = System.getProperty("org.tinyjee.maven.dim.project." + projectPathKey);
167 			if (!StringUtils.isEmpty(projectPath)) {
168 				final Log log = Globals.getLog();
169 				final List<String> paths = new ArrayList<String>(asList(StringUtils.split(projectPath, File.pathSeparator)));
170 				for (Iterator<String> iterator = paths.iterator(); iterator.hasNext(); ) {
171 					final String pp = iterator.next();
172 					if (!isPathBelowBase(basePath, pp)) {
173 						if (log.isDebugEnabled()) {
174 							log.debug("Ignoring project path '" + projectPathKey + ':' + pp + "' is not below the given " +
175 									"search base path of '" + basePath + '\'');
176 						}
177 						iterator.remove();
178 					}
179 				}
180 
181 				if (!paths.isEmpty()) {
182 					result = result.length == paths.size() ? result : new File[paths.size()];
183 					for (int i = 0, len = paths.size(); i < len; i++)
184 						result[i] = new File(paths.get(i) + File.separator + path.substring(projectPathPrefix.length()));
185 				}
186 			}
187 		}
188 
189 		return result;
190 	}
191 
192 	private static boolean isPathBelowBase(String basePath, String path) {
193 		if (!isAbsolute(basePath)) return false;
194 		if (!isAbsolute(path)) return false;
195 		path = path.replace('\\', '/').trim();
196 		basePath = basePath.replace('\\', '/').trim();
197 		boolean caseSensitive = File.separatorChar == '/';
198 		return caseSensitive ? path.startsWith(basePath) : path.toLowerCase().startsWith(basePath.toLowerCase());
199 	}
200 
201 	/**
202 	 * Builds a list of paths to search in.
203 	 *
204 	 * @param basePath      the base path to lookup the search paths against (if not absolute).
205 	 * @param proposedPaths an optional list of proposed paths (!!these paths are user input!!).
206 	 * @return an ordered list of paths to use for searching resources.
207 	 */
208 	public static List<File> buildDefaultSearchPaths(File basePath, String... proposedPaths) {
209 		final List<File> searchPaths = new ArrayList<File>();
210 		final File[] basePaths = {basePath, new File(".")};
211 		final List<String[]> partialPaths = new ArrayList<String[]>();
212 
213 		if (proposedPaths != null && proposedPaths.length > 0) {
214 			for (int i = 0; i < proposedPaths.length; i++)
215 				proposedPaths[i] = toFilePath(proposedPaths[i]);
216 			partialPaths.add(proposedPaths);
217 		}
218 
219 		partialPaths.add(DEFAULT_SITE_SOURCE_SEARCH_PATH);
220 		partialPaths.add(DEFAULT_SOURCE_SEARCH_PATH);
221 		partialPaths.add(new String[]{""});
222 
223 		for (File path : basePaths) {
224 			if (path == null)
225 				continue;
226 
227 			for (String[] paths : partialPaths) {
228 				for (String pp : paths) {
229 					if (pp == null) continue;
230 
231 					for (File searchPath : canonicalizePath(path, pp)) {
232 						if (searchPath.exists() && !searchPaths.contains(searchPath))
233 							searchPaths.add(searchPath.getAbsoluteFile());
234 					}
235 				}
236 			}
237 		}
238 
239 		return searchPaths;
240 	}
241 
242 	/**
243 	 * Returns true if the given filePath string is absolute.
244 	 *
245 	 * @param filePath the filePath to check.
246 	 * @return true if the given filePath string is absolute.
247 	 */
248 	public static boolean isAbsolute(String filePath) {
249 		if (filePath == null)
250 			return false;
251 		if (filePath.startsWith("/") || filePath.startsWith("\\"))
252 			return true;
253 
254 		return File.pathSeparatorChar != ':' && filePath.length() > 1 && filePath.charAt(1) == ':';
255 	}
256 
257 	/**
258 	 * Converts the given source to a filePath if possible.
259 	 *
260 	 * @param source the source to convert.
261 	 * @return the filePath of the given source or 'null' if the source is an URL like 'ftp://' or 'http://'.
262 	 */
263 	public static String toFilePath(String source) {
264 		if (source != null) {
265 			// Normalize to unix paths
266 			source = source.replace('\\', '/');
267 
268 			if (!isAbsolute(source)) {
269 				// Resolve file URIs
270 				try {
271 					URI sourceURI = new URI(source);
272 					if (sourceURI.getScheme() != null) {
273 						if ("file".equalsIgnoreCase(sourceURI.getScheme()))
274 							source = sourceURI.getPath();
275 						else
276 							source = null;
277 					}
278 				} catch (URISyntaxException ignored) {
279 					Globals.getLog().debug(source + " is not a file path, processing it as URL.");
280 				}
281 			}
282 		}
283 
284 		return source;
285 	}
286 
287 	/**
288 	 * Finds the given source within the specified search paths and adds the results as file
289 	 * URLs to the results list.
290 	 *
291 	 * @param searchPaths the paths to search in.
292 	 * @param source      the source path to search for.
293 	 * @param results     a list to append the results to.
294 	 */
295 	public static void findMatchingPaths(List<File> searchPaths, String source, List<URL> results) {
296 		source = toFilePath(source);
297 		if (source == null) return;
298 
299 		for (File searchPath : searchPaths) {
300 			File file = new File(searchPath, source);
301 			if (file.exists())
302 				try {
303 					URL url = file.toURI().toURL();
304 					if (!results.contains(url)) results.add(url);
305 				} catch (MalformedURLException e) {
306 					throw new RuntimeException(e);
307 				}
308 		}
309 	}
310 
311 	/**
312 	 * Resolves the non-file URLs and module path expressions that match the source.
313 	 *
314 	 * @param basePath the basePath of the maven project (e.g. containing the target folder).
315 	 * @param source   the source URI or module path expression.
316 	 * @param results  the result list to write to.
317 	 */
318 	public static void findMatchingURLs(File basePath, String source, List<URL> results) {
319 		if (isAbsolute(source)) return;
320 
321 		try {
322 			if (source.startsWith("[") && source.contains("]:")) {
323 				String moduleName = source.substring(1, source.indexOf("]:"));
324 				String path = getModulePath(moduleName);
325 				if (path != null) {
326 					final List<File> searchPaths = buildDefaultSearchPaths(new File(path));
327 					findMatchingPaths(searchPaths, source.substring(moduleName.length() + 3), results);
328 					assertHasResult(source, results, "below the paths", searchPaths);
329 				} else {
330 					assertHasResult(source, results, "below the module base paths", getModulePaths().entrySet());
331 				}
332 			} else {
333 				URI sourceURI = new URI(source);
334 				String scheme = sourceURI.getScheme();
335 				if (scheme != null && !"file".equalsIgnoreCase(scheme)) {
336 					if ("classpath".equalsIgnoreCase(scheme)) {
337 						URLClassLoader ucl = resolveClassLoader(basePath);
338 						String path = sourceURI.getPath();
339 						if (path.startsWith("/")) path = path.substring(1);
340 						for (Enumeration<URL> e = ucl.getResources(path); e.hasMoreElements(); ) {
341 							URL url = e.nextElement();
342 							if (!results.contains(url)) results.add(url);
343 						}
344 
345 						assertHasResult(source, results,
346 								"inside Maven's site building classpath nor below the paths", asList(ucl.getURLs()));
347 					} else {
348 						results.add(sourceURI.toURL());
349 					}
350 				}
351 			}
352 		} catch (RuntimeException e) {
353 			throw e;
354 		} catch (IOException e) {
355 			Globals.getLog().debug(source + " points to an invalid or broken classpath.", e);
356 		} catch (Exception ignored) {
357 			Globals.getLog().debug(source + " is not an URI, not resolving it as URL.");
358 		}
359 	}
360 
361 	/**
362 	 * Finds the given source relative to the specified basePath.
363 	 * <p/>
364 	 * Note: Sources are resolved using absolute or relative file paths (the latter is using a list
365 	 * of paths to find the resource inside) OR sources can use any supported URI scheme including
366 	 * FTP and HTTP and lastly using the special scheme "CLASSPATH" in order to use the module's
367 	 * classpath to resolve the source.
368 	 *
369 	 * @param basePath the basePath of the maven project (e.g. containing the target folder).
370 	 * @param source   the source path or URI.
371 	 * @return the resolved URL.
372 	 * @throws IllegalArgumentException In case of the source was not resolvable.
373 	 */
374 	public static URL findSource(File basePath, String source) {
375 		final Log log = Globals.getLog();
376 		final List<URL> results = findAllSources(basePath, source);
377 
378 		if (results.size() > 1) {
379 			for (int i = results.size() - 1; i > 0; i--) {
380 				URL url = results.get(i);
381 				if ("file".equalsIgnoreCase(url.getProtocol())) {
382 					url = results.set(0, url);
383 					results.set(i, url);
384 					log.info("Found multiple matching sources " + results + " for " + source + ", preferring the local file over others.");
385 				}
386 			}
387 			log.warn("Found matching sources @ " + results + " for " + source + ", using the first match.");
388 		} else if (log.isDebugEnabled())
389 			log.debug("Found matching source @ " + results + " for " + source + '.');
390 
391 		return results.get(0);
392 	}
393 
394 	/**
395 	 * Finds the given source relative to the specified basePath.
396 	 *
397 	 * @param basePath the basePath of the maven project (e.g. containing the target folder).
398 	 * @param source   the source path or URI.
399 	 * @return all URLs that contain this source.
400 	 */
401 	public static List<URL> findAllSources(File basePath, String source) {
402 		final List<URL> results = new ArrayList<URL>();
403 
404 		findMatchingURLs(basePath, source, results);
405 
406 		if (results.isEmpty()) {
407 			List<File> searchPaths = buildDefaultSearchPaths(basePath);
408 			findMatchingPaths(searchPaths, source, results);
409 			assertHasResult(source, results, "below the paths", searchPaths);
410 		}
411 
412 		return results;
413 	}
414 
415 	private static void assertHasResult(String source, List<URL> results, String where, Collection<?> searchPaths) {
416 		if (results.isEmpty()) {
417 			final String separator = System.getProperty("line.separator");
418 			final StringBuilder builder = new StringBuilder().append("Didn't find ");
419 
420 			builder.append('\'').append(source).append("' ").append(where).append(": ");
421 			for (Object path : searchPaths) builder.append(separator).append("-- ").append(path);
422 
423 			throw new IllegalArgumentException(builder.toString());
424 		}
425 	}
426 
427 	/**
428 	 * Resolves the specified class.
429 	 *
430 	 * @param basePath  the basePath of the maven project (e.g. containing the target folder).
431 	 * @param className the name of the class to resolve.
432 	 * @return The class.
433 	 * @throws IllegalArgumentException In case of the class was not found.
434 	 */
435 	public static Class<?> resolveClass(File basePath, String className) {
436 		try {
437 			return Class.forName(className);
438 		} catch (ClassNotFoundException ignored) {
439 			final URLClassLoader loader = resolveClassLoader(basePath);
440 			try {
441 				return Class.forName(className, true, loader);
442 			} catch (ClassNotFoundException e1) {
443 				throw new IllegalArgumentException("Didn't find the source class '" + className +
444 						"' inside Maven's site building classpath nor below the paths: " +
445 						asList(loader.getURLs()), e1);
446 			}
447 		}
448 	}
449 
450 	/**
451 	 * Resolves a class loader for the given base path.
452 	 *
453 	 * @param basePath the basePath of the maven project (e.g. containing the target folder).
454 	 * @return a URLClassLoader that is capable of loading all classes defined inside the current module.
455 	 *         (without transitive dependencies)
456 	 */
457 	public static synchronized URLClassLoader resolveClassLoader(File basePath) {
458 		if (basePath == null) return (URLClassLoader) ResourceResolver.class.getClassLoader();
459 
460 		// TODO: Implement ".class"-file modified checking as used in UrlFetcher.
461 
462 		final String clKey = basePath.getAbsolutePath();
463 		URLClassLoader ulc = currentClassLoader.get();
464 		if (ulc == null || currentClassLoaderKey == null || !currentClassLoaderKey.equals(clKey)) {
465 			try {
466 				Set<String> classpath = new LinkedHashSet<String>();
467 				classpath.add(System.getProperty("org.tinyjee.maven.dim.project.outputDirectory",
468 						new File(basePath, CLASSES_PATH).getAbsolutePath()));
469 				classpath.add(System.getProperty("org.tinyjee.maven.dim.project.testOutputDirectory",
470 						new File(basePath, TEST_CLASSES_PATH).getAbsolutePath()));
471 
472 				String projectClassPath = System.getProperty("org.tinyjee.maven.dim.include.project.classpath");
473 				if (projectClassPath != null)
474 					Collections.addAll(classpath, StringUtils.split(projectClassPath, File.pathSeparator));
475 
476 				String projectTestClassPath = System.getProperty("org.tinyjee.maven.dim.include.project.test.classpath");
477 				if (projectTestClassPath != null)
478 					Collections.addAll(classpath, StringUtils.split(projectTestClassPath, File.pathSeparator));
479 
480 				Globals.getLog().debug("Creating a new classloader to load classes via 'source-class', using the classpath:" + classpath);
481 
482 				int i = 0;
483 				URL[] urls = new URL[classpath.size()];
484 				for (String file : classpath) urls[i++] = new File(file).toURI().toURL();
485 				ulc = new URLClassLoader(urls, ResourceResolver.class.getClassLoader());
486 				currentClassLoader = new SoftReference<URLClassLoader>(ulc);
487 				currentClassLoaderKey = clKey;
488 			} catch (Exception e) {
489 				throw new RuntimeException("Failed to build search path to lookup .class files.", e);
490 			}
491 		}
492 
493 		return ulc;
494 	}
495 
496 	/**
497 	 * Finds the source directory for the project site.
498 	 *
499 	 * @param baseDir         the base directory of the project.
500 	 * @param proposedSiteDir an optional proposal where the site directory may be.
501 	 * @return the source directory for the project site or 'null' if it wasn't found.
502 	 */
503 	public static File findSiteSourceDirectory(File baseDir, File proposedSiteDir) {
504 		final Log log = Globals.getLog();
505 		final List<File> paths = buildDefaultSearchPaths(baseDir, proposedSiteDir == null ? null : proposedSiteDir.getAbsolutePath());
506 
507 		for (File file : paths) {
508 			if ("site".equals(file.getName())) {
509 				log.debug("Using '" + file + "/resources', to append CSS style sheets.");
510 				return file;
511 			}
512 		}
513 
514 		log.debug("Didn't find site directory below " + paths + ", will inline CSS styles.");
515 		return null;
516 	}
517 
518 	private ResourceResolver() {
519 	}
520 }