View Javadoc

1   package org.scala_tools.maven;
2   
3   import java.io.BufferedReader;
4   import java.io.File;
5   import java.io.FileOutputStream;
6   import java.io.FileReader;
7   import java.io.IOException;
8   import java.io.PrintStream;
9   import java.io.StringReader;
10  import java.lang.reflect.Constructor;
11  import java.lang.reflect.InvocationTargetException;
12  import java.net.MalformedURLException;
13  import java.net.URL;
14  import java.net.URLClassLoader;
15  import java.util.ArrayList;
16  import java.util.Arrays;
17  import java.util.Collection;
18  import java.util.HashSet;
19  import java.util.List;
20  import java.util.Set;
21  
22  import org.apache.maven.artifact.DependencyResolutionRequiredException;
23  import org.apache.maven.artifact.versioning.VersionRange;
24  import org.apache.maven.model.Dependency;
25  import org.apache.maven.plugin.MojoFailureException;
26  import org.apache.maven.plugin.logging.Log;
27  import org.scala_tools.maven.model.MavenProjectAdapter;
28  
29  
30  /**
31   * Run a scala script.
32   * 
33   * @goal script
34   * @requiresDependencyResolution runtime
35   * @executionStrategy always
36   * @since 2.7
37   */
38  public class ScalaScriptMojo extends ScalaMojoSupport {
39  
40  	/*
41  	 * If the maven-scala-project is a dependency of the project then the
42  	 * MavenProject object describing the project will be passed to the script.
43  	 */
44  
45  	/**
46  	 * The build directory of the project
47  	 * 
48  	 * @parameter expression="${project.build.directory}"
49  	 */
50  	protected File outputDir;
51  
52  	/**
53  	 * The file containing script to be executed. Either '<em>scriptFile</em>'
54  	 * or '<em>script</em>' must be defined.
55  	 * 
56  	 * @parameter expression="${scriptFile}"
57  	 */
58  	protected File scriptFile;
59  
60  	/**
61  	 * The script that will be executed. Either '<em>scriptFile</em>' or '
62  	 * <em>script</em>' must be defined.
63  	 * 
64  	 * @parameter expression="${script}"
65  	 */
66  	protected String script;
67  
68  	/**
69  	 * If set to true the Scala classfile that is generated will not be deleted
70  	 * after the goal completes. This is to allows easier debugging of the
71  	 * script especially since line numbers will be wrong because lines are
72  	 * added to the compiled script (see script examples)
73  	 * 
74  	 * @parameter expression="${maven.scala.keepGeneratedScript}"
75  	 *            default-value="false"
76  	 */
77  	protected boolean keepGeneratedScript;
78  
79  	/**
80  	 * Comma separated list of scopes to add to the classpath. Eg: test,compile
81  	 * 
82  	 * @parameter expression="${maven.scala.includeScopes}"
83  	 *            default-value="compile, test, runtime"
84  	 * @required
85  	 */
86  	protected String includeScopes;
87  
88  	/**
89  	 * Comma separated list of scopes to remove from the classpath. Eg:
90  	 * test,compile
91  	 * 
92  	 * @parameter expression="${maven.scala.excludeScopes}"
93  	 */
94  	protected String excludeScopes;
95  
96  	/**
97  	 * Comma seperated list of directories or jars to add to the classpath
98  	 * 
99  	 * @parameter expression="${addToClasspath}"
100 	 */
101 	protected String addToClasspath;
102 
103 	/**
104 	 * Comma separated list of directories or jars to remove from the classpath.
105 	 * This is useful for resolving conflicts in the classpath. For example, the
106 	 * script uses Ant 1.7 and the compiler dependencies pull in Ant 1.5
107 	 * optional which conflicts and causes a crash
108 	 * 
109 	 * @parameter expression="${removeFromClasspath}"
110 	 */
111 	protected String removeFromClasspath;
112 
113 	private static int currentScriptIndex = 0;
114 
115 	@Override
116 	protected void doExecute() throws Exception {
117 		if (script == null && scriptFile == null) {
118 			throw new MojoFailureException(
119 					"Either script or scriptFile must be defined");
120 		}
121 		if (script != null && scriptFile != null) {
122 			throw new MojoFailureException(
123 					"Only one of script or scriptFile can be defined");
124 		}
125 		currentScriptIndex++;
126 
127 		// prepare
128 		File scriptDir = new File(outputDir, ".scalaScriptGen");
129 		scriptDir.mkdirs();
130 		File destFile = new File(scriptDir + "/" + scriptBaseName() + ".scala");
131 
132 		Set<String> classpath = new HashSet<String>();
133 		configureClasspath(classpath);
134 
135 		URLClassLoader loader = createScriptClassloader(scriptDir, classpath);
136 
137 		boolean mavenProjectDependency = hasMavenProjectDependency(classpath);
138 		wrapScript(destFile, mavenProjectDependency);
139 
140 		try {
141 			compileScript(scriptDir, destFile, classpath);
142 			runScript(mavenProjectDependency, loader);
143 		} finally {
144 			if (!keepGeneratedScript) {
145 				delete(scriptDir);
146 			}
147 		}
148 
149 	}
150 
151 	private boolean hasMavenProjectDependency(Set<String> classpath)
152 			throws MalformedURLException {
153 		try {
154 			List<URL> urls = new ArrayList<URL>();
155 
156 			// add the script directory to the classpath
157 			for (String string : classpath) {
158 				urls.add(new URL("file://" + string));
159 			}
160 
161 			URLClassLoader loader = new URLClassLoader(urls
162 					.toArray(new URL[urls.size()]));
163 
164 			loader.loadClass(MavenProjectAdapter.class.getCanonicalName());
165 			return true;
166 		} catch (ClassNotFoundException e) {
167 			return false;
168 		}
169 	}
170 
171 	private void runScript(boolean mavenProjectDependency, URLClassLoader loader)
172 			throws Exception {
173 		Class<?> compiledScript = loader.loadClass(scriptBaseName());
174 
175 		try {
176 			try {
177 				Object instance;
178 				if (mavenProjectDependency) {
179 					Constructor<?> constructor = compiledScript
180 							.getConstructor(MavenProjectAdapter.class, Log.class);
181 					instance = constructor.newInstance(new MavenProjectAdapter(
182 							project), getLog());
183 				} else {
184 					instance = compiledScript.newInstance();
185 				}
186 				try {
187 					compiledScript.getMethod("run").invoke(instance);
188 				} catch (NoSuchMethodException e) {
189 					// ignore because if there is no method then its ok and we
190 					// just
191 					// don't run the method. initialization of the class must
192 					// have been
193 					// enough
194 				}
195 			} catch (InvocationTargetException e) {
196 				if (e.getTargetException() != null) {
197 					throw e.getTargetException();
198 				} else if (e.getCause() != null) {
199 					throw e.getCause();
200 				} else {
201 					throw e;
202 				}
203 			} catch (ExceptionInInitializerError e) {
204 				if (e.getException() != null) {
205 					throw e.getException();
206 				} else if (e.getCause() != null) {
207 					throw e.getCause();
208 				} else {
209 					throw e;
210 				}
211 			}
212 		} catch (Throwable e) {
213 			if (e instanceof Exception) {
214 				throw (Exception) e;
215 			} else {
216 				throw new Exception("A " + e.getClass().getSimpleName()
217 						+ " exception was thrown", e);
218 			}
219 		}
220 	}
221 
222 	private URLClassLoader createScriptClassloader(File scriptDir,
223 			Set<String> classpath) throws MalformedURLException {
224 		List<URL> urls = new ArrayList<URL>();
225 
226 		// add the script directory to the classpath
227 		urls.add(scriptDir.toURI().toURL());
228 
229 		for (String string : classpath) {
230 			urls.add(new URL("file://" + string));
231 		}
232 
233 		URLClassLoader loader = new URLClassLoader(urls.toArray(new URL[urls
234 				.size()]), getClass().getClassLoader());
235 		return loader;
236 	}
237 
238 	private void compileScript(File scriptDir, File destFile,
239 			Set<String> classpath) throws Exception {
240 		JavaCommand jcmd = getScalaCommand();
241 		jcmd.addArgs("-classpath", JavaCommand
242 				.toMultiPath(new ArrayList<String>(classpath)));
243 		jcmd.addArgs("-d", scriptDir.getAbsolutePath());
244 		jcmd.addArgs("-sourcepath", scriptDir.getAbsolutePath());
245 		jcmd.addArgs(destFile.getAbsolutePath());
246 
247 		jcmd.run(displayCmd);
248 	}
249 
250 	private void configureClasspath(Set<String> classpath) throws Exception,
251 			DependencyResolutionRequiredException {
252 		MavenProjectAdapter projectAdapter = new MavenProjectAdapter(project);
253 	
254 		Collection<Dependency> toInclude = new ArrayList<Dependency>();
255 		if (includeScopes == null || includeScopes.length() == 0) {
256 			getLog().warn("No scopes were included");
257 		} else {
258 
259 			String[] include = includeScopes.split(",");
260 			for (String string : include) {
261 				Scopes scope = Scopes.lookup(string.toUpperCase());
262 				if (scope != null) {
263 					toInclude.addAll(scope.elements(projectAdapter));
264 				} else {
265 					getLog().warn(
266 							"Included Scope: " + string + " is not one of: "
267 									+ Arrays.asList(Scopes.values()));
268 				}
269 			}
270 		}
271 		if (excludeScopes != null && excludeScopes.length() > 0) {
272 
273 			String[] exclude = excludeScopes.split(",");
274 			for (String string : exclude) {
275 				Scopes scope = Scopes.lookup(string.toUpperCase());
276 				if (scope != null) {
277 					toInclude.removeAll(scope.elements(projectAdapter));
278 				} else {
279 					getLog().warn(
280 							"Excluded Scope: " + string + " is not one of: "
281 									+ Arrays.asList(Scopes.values()));
282 				}
283 			}
284 		}
285 
286 		for (Dependency dependency : toInclude) {
287 			addToClasspath(factory.createDependencyArtifact(dependency
288 					.getGroupId(), dependency.getArtifactId(), VersionRange
289 					.createFromVersion(dependency.getVersion()), dependency
290 					.getType(), dependency.getClassifier(), dependency
291 					.getScope(), dependency.isOptional()), classpath);
292 		}
293 
294 
295 		
296 		if (addToClasspath != null) {
297 			classpath.addAll(Arrays.asList(addToClasspath.split(",")));
298 		}
299 		
300 		if (removeFromClasspath != null) {
301 			ArrayList<String> toRemove = new ArrayList<String>();
302 			String[] jars = removeFromClasspath.trim().split(",");
303 			for (String string : classpath) {
304 				for (String jar : jars) {
305 					if (string.contains(jar.trim())) {
306 						toRemove.add(string);
307 					}
308 				}
309 			}
310 			classpath.removeAll(toRemove);
311 		}
312  		
313 		String outputDirectory = project.getBuild().getOutputDirectory();
314 		if(!outputDirectory.endsWith("/")){
315 		    // need it to end with / for URLClassloader
316 		    outputDirectory+="/";
317 		}
318         classpath.add( outputDirectory);
319 		addToClasspath("org.scala-lang", "scala-compiler", scalaVersion,
320 				classpath);
321 		addToClasspath("org.scala-lang", "scala-library", scalaVersion,
322 				classpath);
323 				
324 		getLog().debug("Using the following classpath for running and compiling scripts: "+classpath);
325 
326 	}
327 
328 	private void wrapScript(File destFile, boolean mavenProjectDependency)
329 			throws IOException {
330 		destFile.delete();
331 
332 		FileOutputStream fileOutputStream = new FileOutputStream(destFile);
333 		PrintStream out = new PrintStream(fileOutputStream);
334 		try {
335 			BufferedReader reader;
336 			if (scriptFile != null) {
337 				reader = new BufferedReader(new FileReader(scriptFile));
338 			} else {
339 				reader = new BufferedReader(new StringReader(script));
340 			}
341 
342 			if (mavenProjectDependency) {
343 				out.println("import scala.collection.jcl.Conversions._");
344 				out.println("class " + scriptBaseName() + "(project:"
345 						+ MavenProjectAdapter.class.getCanonicalName() + ",log:"+Log.class.getCanonicalName()+") {");
346 			} else {
347 				out.println("class " + scriptBaseName() + " {");
348 			}
349 
350 			String line = reader.readLine();
351 			while (line != null) {
352 				out.print("  ");
353 				out.println(line);
354 				line = reader.readLine();
355 			}
356 
357 			out.println("}");
358 		} finally {
359 			out.close();
360 			fileOutputStream.close();
361 		}
362 	}
363 
364 	private String scriptBaseName() {
365 		if (scriptFile == null) {
366 			return "embeddedScript_" + currentScriptIndex;
367 		} else {
368 			int dot = scriptFile.getName().lastIndexOf('.');
369 			if (dot == -1) {
370 				return scriptFile.getName() + "_" + currentScriptIndex;
371 			}
372 			return scriptFile.getName().substring(0, dot) + "_"
373 					+ currentScriptIndex;
374 		}
375 	}
376 
377 	private void delete(File scriptDir) {
378 		if (scriptDir.isDirectory()) {
379 			for (File file : scriptDir.listFiles()) {
380 				delete(file);
381 			}
382 		}
383 
384 		scriptDir.deleteOnExit();
385 		scriptDir.delete();
386 	}
387 
388 	private enum Scopes {
389 		COMPILE {
390 			public Collection<Dependency> elements(MavenProjectAdapter project)
391 					throws DependencyResolutionRequiredException {
392 				return project.getCompileDependencies();
393 			}
394 		},
395 		RUNTIME {
396 			public Collection<Dependency> elements(MavenProjectAdapter project)
397 					throws DependencyResolutionRequiredException {
398 				return project.getRuntimeDependencies();
399 			}
400 		},
401 		TEST {
402 			public Collection<Dependency> elements(MavenProjectAdapter project)
403 					throws DependencyResolutionRequiredException {
404 				return project.getTestDependencies();
405 			}
406 		},
407 		SYSTEM {
408 			public Collection<Dependency> elements(MavenProjectAdapter project)
409 					throws DependencyResolutionRequiredException {
410 				return project.getSystemDependencies();
411 			}
412 		};
413 
414 		public abstract Collection<Dependency> elements(
415 				MavenProjectAdapter project)
416 				throws DependencyResolutionRequiredException;
417 
418 		public static Scopes lookup(String name) {
419 			for (Scopes scope : Scopes.values()) {
420 				if (scope.name().trim().equalsIgnoreCase(name.trim())) {
421 					return scope;
422 				}
423 			}
424 			return null;
425 		}
426 	}
427 }