View Javadoc
1   package org.codehaus.mojo.sqlj;
2   
3   import java.io.File;
4   import java.io.IOException;
5   import java.io.PrintWriter;
6   import java.io.StringWriter;
7   import java.net.URL;
8   import java.net.URLClassLoader;
9   import java.util.ArrayList;
10  import java.util.Arrays;
11  import java.util.HashSet;
12  import java.util.List;
13  import java.util.Set;
14  
15  import org.apache.commons.beanutils.MethodUtils;
16  import org.apache.commons.io.FileUtils;
17  import org.apache.commons.lang.StringUtils;
18  import org.apache.commons.lang.SystemUtils;
19  import org.apache.maven.model.Resource;
20  import org.apache.maven.plugin.MojoExecutionException;
21  import org.apache.maven.plugin.MojoFailureException;
22  import org.apache.maven.plugins.annotations.Component;
23  import org.apache.maven.plugins.annotations.LifecyclePhase;
24  import org.apache.maven.plugins.annotations.Mojo;
25  import org.apache.maven.plugins.annotations.Parameter;
26  import org.apache.maven.plugins.annotations.ResolutionScope;
27  import org.apache.maven.project.MavenProject;
28  import org.codehaus.plexus.util.Scanner;
29  import org.sonatype.plexus.build.incremental.BuildContext;
30  
31  /**
32   * Translates SQLJ source code using the SQLJ Translator.
33   * 
34   * @author <a href="mailto:david@codehaus.org">David J. M. Karlsen</a>
35   */
36  @Mojo( name = "sqlj", defaultPhase = LifecyclePhase.GENERATE_SOURCES, requiresDependencyResolution = ResolutionScope.COMPILE )
37  public class SqljMojo
38      extends AbstractSqljMojo
39  {
40  
41      private static final String SQLJ_CLASS = "sqlj.tools.Sqlj";
42      
43      /**
44       * Codepage for generated sources.
45       */
46      @Parameter( property = "sqlj.encoding", defaultValue = "${project.build.sourceEncoding}" )
47      private String encoding;
48  
49      /**
50       * Show detailed information on SQLJ processing. (Is automatically shown if Maven is executed in debug mode.)
51       * 
52       * @since 1.2
53       */
54      @Parameter( property = "sqlj.verbose", defaultValue = "false" )
55      private boolean verbose;
56  
57      /**
58       * Explicit list of SQLJ files to process.
59       */
60      @Parameter( property = "sqlj.sqljFiles" )
61      private File[] sqljFiles;
62  
63      /**
64       * Directories to recursively scan for SQLJ files (only files with .sqlj extension are included).
65       */
66      @Parameter( property = "sqlj.sqljDirectories" )
67      private File[] sqljDirs;
68      
69      /**
70       * Additional arguments to pass to the SQLJ translator.
71       */
72      @Parameter ( property = "sqlj.additionalArgs")
73      private List<String> additionalArgs;
74  
75      /**
76       * The enclosing project.
77       */
78      @Component
79      private MavenProject mavenProject;
80  
81      /**
82       * For m2e compatibility.
83       */
84      @Component
85      private BuildContext buildContext;
86  
87      /**
88       * {@inheritDoc}
89       */
90      public void execute()
91          throws MojoExecutionException, MojoFailureException
92      {
93          URLClassLoader sqljClassLoader = getSqljClassLoader();
94          if ( getLog().isDebugEnabled() )
95          {
96              getLog().debug( "SQLJ classpath:" );
97              for ( URL url : sqljClassLoader.getURLs() )
98              {
99                  getLog().debug( " " + url.getPath() );
100             }
101         }
102 
103         String sqljVersionInfo = getSqljVersionInfo( sqljClassLoader );
104         if ( sqljVersionInfo != null )
105         {
106             getLog().info( "Using SQLJ Translator version '" + sqljVersionInfo + "'" );
107         }
108         else
109         {
110             getLog().info( "Couldn't retrieve SQLJ Translator version info" );
111         }
112 
113         if ( !checkSqljDirAndFileDeclarations() )
114         {
115             String msg = "Plugin configuration contains invalid SQLJ directory or file declaration(s).";
116             throw new MojoExecutionException( msg );
117         }
118 
119         if ( StringUtils.isEmpty( encoding ) )
120         {
121             encoding = SystemUtils.FILE_ENCODING;
122             getLog().warn( "No encoding given, falling back to system default value: " + encoding );
123         }
124 
125         try
126         {
127             FileUtils.forceMkdir( getGeneratedResourcesDirectory().getAbsoluteFile() );
128             FileUtils.forceMkdir( getGeneratedSourcesDirectory().getAbsoluteFile() );
129         }
130         catch ( IOException e )
131         {
132             throw new MojoFailureException( e.getMessage() );
133         }
134 
135         Set<File> staleSqljFiles = getStaleSqljFiles();
136         boolean translationPerformed;
137         if ( !staleSqljFiles.isEmpty() )
138         {
139             for ( File file : staleSqljFiles )
140             {
141                 buildContext.removeMessages( file );
142                 try
143                 {
144                     translate( file, sqljClassLoader );
145                 }
146                 catch ( MojoExecutionException e )
147                 {
148                     final String msg = "Error translating SQLJ file";
149                     buildContext.addMessage( file, 0, 0, msg, BuildContext.SEVERITY_ERROR, e );
150                     throw e;
151                 }
152             }
153             translationPerformed = true;
154             final int numberOfFiles = staleSqljFiles.size();
155             getLog().info( "Translated " + numberOfFiles + " SQLJ file" + ( numberOfFiles > 0 ? "s." : "." ) );
156         }
157         else
158         {
159             getLog().info( "No updated SQLJ files found - skipping SQLJ translation." );
160             translationPerformed = false;
161         }
162 
163         Resource resource = new Resource();
164         resource.setDirectory( getGeneratedResourcesDirectory().getAbsolutePath() );
165         mavenProject.addResource( resource );
166         if ( translationPerformed )
167         {
168             buildContext.refresh( getGeneratedResourcesDirectory() );
169         }
170         mavenProject.addCompileSourceRoot( getGeneratedSourcesDirectory().getAbsolutePath() );
171         if ( translationPerformed )
172         {
173             buildContext.refresh( getGeneratedSourcesDirectory() );
174         }
175     }
176 
177     private List<URL> getProjectClasspath()
178         throws Exception
179     {
180         @SuppressWarnings( "unchecked" )
181         final List<String> classpathElements = mavenProject.getCompileClasspathElements();
182         List<URL> result = new ArrayList<URL>( classpathElements.size() );
183         for ( String s : classpathElements )
184         {
185             File tmp = new File( s );
186             result.add( tmp.toURI().toURL() );
187         }
188 
189         return result;
190     }
191 
192     private URLClassLoader getSqljClassLoader()
193         throws MojoExecutionException, MojoFailureException
194     {
195         ClassLoader pluginClassLoader = Thread.currentThread().getContextClassLoader();
196         if ( pluginClassLoader instanceof URLClassLoader == false )
197         {
198             throw new MojoFailureException( "Unexpected error: Class loader is not of URLClassLoader type" );
199         }
200         URLClassLoader pluginURLClassLoader = (URLClassLoader) pluginClassLoader;
201 
202         // This is the classpath we want:
203         // 1. the plugin's classpath
204         // 2. the project's compile classpath
205         URL[] pluginClassLoaderURLs = pluginURLClassLoader.getURLs();
206         List<URL> projectClasspathURLs;
207         try
208         {
209             projectClasspathURLs = getProjectClasspath();
210         }
211         catch ( Exception e )
212         {
213             throw new MojoExecutionException( "Couldn't retrieve classpath for project", e );
214         }
215 
216         List<URL> tmp = new ArrayList<URL>( pluginClassLoaderURLs.length + projectClasspathURLs.size() );
217         for ( URL url : pluginClassLoaderURLs )
218         {
219             tmp.add( url );
220         }
221         tmp.addAll( projectClasspathURLs );
222 
223         URLClassLoader newClassLoader = new URLClassLoader( tmp.toArray( new URL[0] ) );
224         return newClassLoader;
225     }
226 
227     private boolean checkSqljDirAndFileDeclarations()
228     {
229         boolean isOk = true;
230 
231         for ( File sqljDir : sqljDirs )
232         {
233             if ( !sqljDir.exists() )
234             {
235                 getLog().error( "Declared SQLJ directory does not exist: " + sqljDir );
236                 isOk = false;
237             }
238             if ( !sqljDir.isDirectory() )
239             {
240                 getLog().error( "Declared SQLJ directory is not a directory: " + sqljDir );
241                 isOk = false;
242             }
243         }
244         for ( File sqljFile : sqljFiles )
245         {
246             if ( !sqljFile.exists() )
247             {
248                 getLog().error( "Declared SQLJ file does not exist: " + sqljFile );
249                 isOk = false;
250             }
251             if ( !sqljFile.isFile() )
252             {
253                 getLog().error( "Declared SQLJ file is not a file: " + sqljFile );
254                 isOk = false;
255             }
256         }
257 
258         return isOk;
259     }
260 
261     /**
262      * Executes the SQLJ Translator on the given file.
263      * 
264      * @param file the file to translate
265      * @param classLoader the class loader to use for loading the SQLJ Translator class with
266      * @throws MojoFailureException in case of failure.
267      * @throws MojoExecutionException in case of execution failure.
268      */
269     private void translate( File file, ClassLoader classLoader )
270         throws MojoFailureException, MojoExecutionException
271     {
272         Class<?> sqljClass;
273         try
274         {
275             sqljClass = classLoader.loadClass( SQLJ_CLASS );
276         }
277         catch ( ClassNotFoundException e )
278         {
279             throw new MojoFailureException( "Please add SQLJ Translator to the plugin's classpath: " + e.getMessage() );
280         }
281         catch ( Exception e )
282         {
283             throw new MojoFailureException( e.getMessage() );
284         }
285 
286         List<String> sqljArgs = new ArrayList<String>();
287         sqljArgs.add( "-dir=" + getGeneratedSourcesDirectory().getAbsolutePath() );
288         sqljArgs.add( "-d=" + getGeneratedResourcesDirectory().getAbsolutePath() );
289         sqljArgs.add( "-encoding=" + encoding );
290         if ( verbose || getLog().isDebugEnabled() )
291         {
292             sqljArgs.add( "-status" );
293         }
294         sqljArgs.add( "-compile=false" );
295         sqljArgs.add( file.getAbsolutePath() );
296         sqljArgs.addAll(additionalArgs);
297 
298         Integer returnCode = null;
299         try
300         {
301             if ( getLog().isDebugEnabled() )
302             {
303                 getLog().debug( "Performing SQLJ translation on " + file );
304             }
305             Object[] methodArgs = new Object[] { sqljArgs.toArray( new String[0] ) };
306             returnCode = (Integer) MethodUtils.invokeExactStaticMethod( sqljClass, "statusMain", methodArgs );
307         }
308         catch ( Exception e )
309         {
310             throw new MojoFailureException( e.getMessage() );
311         }
312 
313         if ( returnCode.intValue() != 0 )
314         {
315             throw new MojoExecutionException( "Can't translate file (return code: " + returnCode + ")" );
316         }
317     }
318 
319     /**
320      * Returns a set of SQLJ source files, based on configured directories and files, that has been modified and
321      * therefore needs to be re-translated.
322      */
323     private Set<File> getStaleSqljFiles()
324         throws MojoExecutionException
325     {
326         Set<File> staleFiles = new HashSet<File>();
327 
328         // Check for modified files in configured directory declarations
329         final String[] sqljIncludes = new String[] { "**/*.sqlj" };
330         for ( File sqljDir : sqljDirs )
331         {
332             if ( getLog().isDebugEnabled() )
333             {
334                 getLog().debug( "Checking for updated SQLJ files in directory: " + sqljDir );
335             }
336             Scanner modifiedScanner = this.buildContext.newScanner( sqljDir );
337             modifiedScanner.setIncludes( sqljIncludes );
338             modifiedScanner.setExcludes( null );
339             modifiedScanner.scan();
340 
341             String[] modifiedFiles = modifiedScanner.getIncludedFiles();
342             for ( String path : modifiedFiles )
343             {
344                 File file = new File( sqljDir, path );
345                 if ( getLog().isDebugEnabled() )
346                 {
347                     getLog().debug( "Updated SQLJ file found: " + file );
348                 }
349                 if ( !staleFiles.add( file ) )
350                 {
351                     getLog().warn( "Duplicated declaration of SQLJ source detected in plugin configuration: " + file );
352                 }
353             }
354         }
355 
356         // Check for modified files in configured file declarations
357         for ( File sqljFile : sqljFiles )
358         {
359             if ( getLog().isDebugEnabled() )
360             {
361                 getLog().debug( "Checking if SQLJ file has been updated: " + sqljFile );
362             }
363             Scanner modifiedScanner = this.buildContext.newScanner( sqljFile.getParentFile() );
364             modifiedScanner.setIncludes( new String[] { sqljFile.getName() } );
365             modifiedScanner.setExcludes( null );
366             modifiedScanner.scan();
367 
368             String[] modifiedFiles = modifiedScanner.getIncludedFiles();
369             if ( modifiedFiles.length == 1 )
370             {
371                 String path = modifiedFiles[0];
372                 File file = new File( sqljFile.getParentFile(), path );
373                 if ( file.compareTo( sqljFile ) == 0 )
374                 {
375                     if ( getLog().isDebugEnabled() )
376                     {
377                         getLog().debug( "Updated SQLJ file found: " + sqljFile );
378                     }
379                     if ( !staleFiles.add( file ) )
380                     {
381                         getLog().warn( "Duplicated declaration of SQLJ source detected in plugin configuration: "
382                                            + file );
383                     }
384                 }
385                 else
386                 {
387                     // Should never happen...
388                     getLog().error( "Unexpected SQLJ file returned; aborting..." );
389                     throw new MojoExecutionException( "Unexpected SQLJ file returned when examining " + sqljFile + ": "
390                         + file );
391                 }
392             }
393             else if ( modifiedFiles.length > 1 )
394             {
395                 // Should never happen...
396                 getLog().error( "Unexpected list of SQLJ files returned; aborting..." );
397                 throw new MojoExecutionException( "Unexpected list of SQLJ files returned when examining " + sqljFile
398                     + ": " + Arrays.toString( modifiedFiles ) );
399             }
400         }
401 
402         // Ignoring checking for deleted files. User should run "mvn clean" to handle that, consistent with
403         // how it works if Java source files are deleted.
404 
405         return staleFiles;
406     }
407 
408     private String getSqljVersionInfo( ClassLoader classLoader )
409     {
410         try 
411         {
412             Class<?> sqljClass = classLoader.loadClass( SQLJ_CLASS );
413             String version = null;
414             try
415             {
416                 version = (String) MethodUtils.invokeExactStaticMethod( sqljClass, "getVersion", null );
417             }
418             catch ( NoSuchMethodException e ) 
419             {
420                 StringWriter stringWriter = new StringWriter();
421                 MethodUtils.invokeExactStaticMethod( sqljClass, "printVersion", new PrintWriter( stringWriter ) );
422                 version = stringWriter.toString();
423             }
424             return version;
425         }
426         catch ( Exception e )
427         {
428             return null;
429         }
430     }
431     
432 }