001/**
002 * Copyright (C) 2009-2011 FuseSource Corp.
003 * http://fusesource.com
004 * 
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 * 
009 *    http://www.apache.org/licenses/LICENSE-2.0
010 * 
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.fusesource.hawtjni.maven;
018
019import java.io.File;
020import java.io.IOException;
021import java.io.Reader;
022import java.net.URL;
023import java.util.ArrayList;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027import java.util.Set;
028
029import org.apache.maven.artifact.Artifact;
030import org.apache.maven.plugin.AbstractMojo;
031import org.apache.maven.plugin.MojoExecutionException;
032import org.apache.maven.project.MavenProject;
033import org.codehaus.plexus.interpolation.InterpolatorFilterReader;
034import org.codehaus.plexus.interpolation.MapBasedValueSource;
035import org.codehaus.plexus.interpolation.StringSearchInterpolator;
036import org.codehaus.plexus.util.FileUtils;
037import org.codehaus.plexus.util.FileUtils.FilterWrapper;
038import org.fusesource.hawtjni.generator.HawtJNI;
039import org.fusesource.hawtjni.generator.ProgressMonitor;
040
041/**
042 * This goal generates the native source code and a
043 * autoconf/msbuild based build system needed to 
044 * build a JNI library for any HawtJNI annotated
045 * classes in your maven project.
046 * 
047 * @goal generate
048 * @phase process-classes
049 * @author <a href="http://hiramchirino.com">Hiram Chirino</a>
050 */
051public class GenerateMojo extends AbstractMojo {
052
053    /**
054     * The maven project.
055     * 
056     * @parameter expression="${project}"
057     * @required
058     * @readonly
059     */
060    protected MavenProject project;
061
062    /**
063     * The directory where the native source files are located.
064     *
065     * @parameter
066     */
067    private File nativeSourceDirectory;
068
069    /**
070     * The directory where the generated native source files are located.
071     * 
072     * @parameter default-value="${project.build.directory}/generated-sources/hawtjni/native-src"
073     */
074    private File generatedNativeSourceDirectory;
075
076    /**
077     * The base name of the library, used to determine generated file names.
078     * 
079     * @parameter default-value="${project.artifactId}"
080     */
081    private String name;
082
083    /**
084     * The copyright header template that will be added to the generated source files.
085     * Use the '%END_YEAR%' token to have it replaced with the current year.  
086     * 
087     * @parameter default-value=""
088     */
089    private String copyright;
090
091    /**
092     * Restrict looking for JNI classes to the specified package.
093     *  
094     * @parameter
095     */
096    private List<String> packages = new ArrayList<String>();
097
098    /**
099     * The directory where the java classes files are located.
100     * 
101     * @parameter default-value="${project.build.outputDirectory}"
102     */
103    private File classesDirectory;
104    
105    /**
106     * The directory where the generated build package is located..
107     * 
108     * @parameter default-value="${project.build.directory}/generated-sources/hawtjni/native-package"
109     */
110    private File packageDirectory;
111    
112    /**
113     * The list of additional files to be included in the package will be
114     * placed.
115     * 
116     * @parameter default-value="${basedir}/src/main/native-package"
117     */
118    private File customPackageDirectory;
119
120    /**
121     * The text encoding of the files.
122     * 
123     * @parameter default-value="UTF-8"
124     */
125    private String encoding;
126
127    /**
128     * Should we skip executing the autogen.sh file.
129     * 
130     * @parameter default-value="${skip-autogen}"
131     */
132    private boolean skipAutogen;
133    
134    /**
135     * Should we force executing the autogen.sh file.
136     * 
137     * @parameter default-value="${force-autogen}"
138     */
139    private boolean forceAutogen;
140
141    /**
142     * Should we display all the native build output?
143     * 
144     * @parameter default-value="${hawtjni-verbose}"
145     */
146    private boolean verbose;
147
148    /**
149     * Extra arguments you want to pass to the autogen.sh command.
150     * 
151     * @parameter
152     */
153    private List<String> autogenArgs;
154    
155    /**
156     * Set this value to false to disable the callback support in HawtJNI.
157     * Disabling callback support can substantially reduce the size
158     * of the generated native library.  
159     * 
160     * @parameter default-value="true"
161     */
162    private boolean callbacks;
163    
164    /**
165     * The build tool to use on Windows systems.  Set
166     * to 'msbuild', 'vcbuild', or 'detect'
167     *
168     * @parameter default-value="detect"
169     */
170    private String windowsBuildTool;
171
172    /**
173     * The name of the msbuild/vcbuild project to use.
174     * Defaults to 'vs2010' for 'msbuild'
175     * and 'vs2008' for 'vcbuild'.
176     *
177     * @parameter
178     */
179    private String windowsProjectName;
180
181    private File targetSrcDir;
182    
183    private CLI cli = new CLI();
184
185    public void execute() throws MojoExecutionException {
186        cli.verbose = verbose;
187        cli.log = getLog();
188        if (nativeSourceDirectory == null) {
189            generateNativeSourceFiles();
190        } else {
191            copyNativeSourceFiles();
192        }
193        generateBuildSystem();
194    }
195
196    private void copyNativeSourceFiles() throws MojoExecutionException {
197        try {
198            FileUtils.copyDirectory(nativeSourceDirectory, generatedNativeSourceDirectory);
199        } catch (Exception e) {
200            throw new MojoExecutionException("Copy of Native source failed: "+e, e);
201        }
202    }
203
204    private void generateNativeSourceFiles() throws MojoExecutionException {
205        HawtJNI generator = new HawtJNI();
206        generator.setClasspaths(getClasspath());
207        generator.setName(name);
208        generator.setCopyright(copyright);
209        generator.setNativeOutput(generatedNativeSourceDirectory);
210        generator.setPackages(packages);
211        generator.setCallbacks(callbacks);
212        generator.setProgress(new ProgressMonitor() {
213            public void step() {
214            }
215            public void setTotal(int total) {
216            }
217            public void setMessage(String message) {
218                getLog().info(message);
219            }
220        });
221        try {
222            generator.generate();
223        } catch (Exception e) {
224            throw new MojoExecutionException("Native source code generation failed: "+e, e);
225        }
226    }
227
228    private void generateBuildSystem() throws MojoExecutionException {
229        try {
230            packageDirectory.mkdirs();
231            new File(packageDirectory, "m4").mkdirs();
232            targetSrcDir = new File(packageDirectory, "src");
233            targetSrcDir.mkdirs();
234
235            if( customPackageDirectory!=null && customPackageDirectory.isDirectory() ) {
236                FileUtils.copyDirectoryStructureIfModified(customPackageDirectory, packageDirectory);
237            }
238
239            if( generatedNativeSourceDirectory!=null && generatedNativeSourceDirectory.isDirectory() ) {
240                FileUtils.copyDirectoryStructureIfModified(generatedNativeSourceDirectory, targetSrcDir);
241            }
242            
243            copyTemplateResource("readme.md", false);
244            copyTemplateResource("configure.ac", true);
245            copyTemplateResource("Makefile.am", true);
246            copyTemplateResource("m4/custom.m4", false);
247            copyTemplateResource("m4/jni.m4", false);
248            copyTemplateResource("m4/osx-universal.m4", false);
249
250            // To support windows based builds..
251            String tool = windowsBuildTool.toLowerCase().trim();
252            if( "detect".equals(tool) ) {
253                copyTemplateResource("vs2008.vcproj", (windowsProjectName != null ? windowsProjectName : "vs2008") + ".vcproj", true);
254                copyTemplateResource("vs2010.vcxproj", (windowsProjectName != null ? windowsProjectName : "vs2010") + ".vcxproj", true);
255            } else if( "msbuild".equals(tool) ) {
256                copyTemplateResource("vs2010.vcxproj", (windowsProjectName != null ? windowsProjectName : "vs2010") + ".vcxproj", true);
257            } else if( "vcbuild".equals(tool) ) {
258                copyTemplateResource("vs2008.vcproj", (windowsProjectName != null ? windowsProjectName : "vs2008") + ".vcproj", true);
259            } else if( "none".equals(tool) ) {
260            } else {
261                throw new MojoExecutionException("Invalid setting for windowsBuildTool: "+windowsBuildTool);
262            }
263
264            File autogen = new File(packageDirectory, "autogen.sh");
265            File configure = new File(packageDirectory, "configure");
266            if( !autogen.exists() ) {
267                copyTemplateResource("autogen.sh", false);
268                cli.setExecutable(autogen);
269            }
270            if( !skipAutogen ) {
271                if( (!configure.exists() && !CLI.IS_WINDOWS) || forceAutogen ) {
272                    try {
273                        cli.system(packageDirectory, new String[] {"./autogen.sh"}, autogenArgs);
274                    } catch (Exception e) {
275                        e.printStackTrace();
276                    }
277                }
278            }
279            
280            
281        } catch (Exception e) {
282            throw new MojoExecutionException("Native build system generation failed: "+e, e);
283        }
284    }
285
286    @SuppressWarnings("unchecked")
287    private ArrayList<String> getClasspath() throws MojoExecutionException {
288        ArrayList<String> artifacts = new ArrayList<String>();
289        try {
290            artifacts.add(classesDirectory.getCanonicalPath());
291            for (Artifact artifact : (Set<Artifact>) project.getArtifacts()) {
292                File file = artifact.getFile();
293                getLog().debug("Including: " + file);
294                artifacts.add(file.getCanonicalPath());
295            }
296        } catch (IOException e) {
297            throw new MojoExecutionException("Could not determine project classath.", e);
298        }
299        return artifacts;
300    }
301
302    private void copyTemplateResource(String file, boolean filter) throws MojoExecutionException {
303        copyTemplateResource(file, file, filter);
304    }
305
306    private void copyTemplateResource(String file, String output, boolean filter) throws MojoExecutionException {
307        try {
308            File target = FileUtils.resolveFile(packageDirectory, output);
309            if( target.isFile() && target.canRead() ) {
310                return;
311            }
312            URL source = getClass().getClassLoader().getResource("project-template/" + file);
313            File tmp = FileUtils.createTempFile("tmp", "txt", new File(project.getBuild().getDirectory()));
314            try {
315                FileUtils.copyURLToFile(source, tmp);
316                FileUtils.copyFile(tmp, target, encoding, filters(filter), true);
317            } finally {
318                tmp.delete();
319            }
320        } catch (IOException e) {
321            throw new MojoExecutionException("Could not extract template resource: "+file, e);
322        }
323    }
324
325    @SuppressWarnings("unchecked")
326    private FilterWrapper[] filters(boolean filter) throws IOException {
327        if( !filter ) {
328            return new FilterWrapper[0];
329        }
330
331        final String startExp = "@";
332        final String endExp = "@";
333        final String escapeString = "\\";
334        final Map<String,String> values = new HashMap<String,String>();
335        values.put("PROJECT_NAME", name);
336        values.put("PROJECT_NAME_UNDER_SCORE", name.replaceAll("\\W", "_"));
337        values.put("VERSION", project.getVersion());
338        
339        List<String> cpp_files = new ArrayList<String>();
340        cpp_files.addAll(FileUtils.getFileNames(targetSrcDir, "**/*.cpp", null, false));
341        cpp_files.addAll(FileUtils.getFileNames(targetSrcDir, "**/*.cxx", null, false));
342
343        List<String> files = new ArrayList<String>();
344        files.addAll(cpp_files);
345        files.addAll(FileUtils.getFileNames(targetSrcDir, "**/*.c", null, false));
346        files.addAll(FileUtils.getFileNames(targetSrcDir, "**/*.m", null, false));
347        String sources = "";
348        String xml_sources = "";
349        String vs10_sources = "";
350        boolean first = true;
351        for (String f : files) {
352            if( !first ) {
353                sources += "\\\n";
354            } else {
355                values.put("FIRST_SOURCE_FILE", "src/"+f.replace('\\', '/'));
356                first=false;
357            }
358            sources += "  src/"+f;
359            
360            xml_sources+="      <File RelativePath=\".\\src\\"+ (f.replace('/', '\\')) +"\"/>\n";
361            vs10_sources+="    <ClCompile Include=\".\\src\\"+ (f.replace('/', '\\')) +"\"/>\n";
362        }
363
364        if( cpp_files.isEmpty() ) {
365            values.put("AC_PROG_CHECKS", "AC_PROG_CC");
366        } else {
367            values.put("AC_PROG_CHECKS", "AC_PROG_CXX");
368        }
369
370        values.put("PROJECT_SOURCES", sources);
371        values.put("PROJECT_XML_SOURCES", xml_sources);
372        values.put("PROJECT_VS10_SOURCES", vs10_sources);
373
374        FileUtils.FilterWrapper wrapper = new FileUtils.FilterWrapper() {
375            public Reader getReader(Reader reader) {
376                StringSearchInterpolator propertiesInterpolator = new StringSearchInterpolator(startExp, endExp);
377                propertiesInterpolator.addValueSource(new MapBasedValueSource(values));
378                propertiesInterpolator.setEscapeString(escapeString);
379                InterpolatorFilterReader interpolatorFilterReader = new InterpolatorFilterReader(reader, propertiesInterpolator, startExp, endExp);
380                interpolatorFilterReader.setInterpolateWithPrefixPattern(false);
381                return interpolatorFilterReader;
382            }
383        };
384        return new FilterWrapper[] { wrapper };
385    }
386    
387
388}