/* ========================================================================== * Copyright Milos Kleint * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ========================================================================= */ package org.codehaus.mojo.nbm; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.JarOutputStream; import java.util.jar.Pack200; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.CRC32; import java.util.zip.GZIPInputStream; import java.util.zip.ZipEntry; import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.factory.ArtifactFactory; import org.apache.maven.artifact.repository.ArtifactRepository; import org.apache.maven.artifact.resolver.ArtifactResolver; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.project.MavenProject; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.Project; import org.apache.tools.ant.filters.StringInputStream; import org.apache.tools.ant.taskdefs.Chmod; import org.apache.tools.ant.types.FileSet; import org.codehaus.plexus.util.FileUtils; import org.codehaus.plexus.util.IOUtil; import org.codehaus.plexus.util.io.InputStreamFacade; import org.netbeans.nbbuild.MakeListOfNBM; /** * Create the Netbeans module clusters/application for the 'nbm-application' packaging * projects * * @author <a href="mailto:mkleint@codehaus.org">Milos Kleint</a> * @goal cluster-app * @phase package * @requiresDependencyResolution runtime * @requiresProject */ public class CreateClusterAppMojo extends AbstractNbmMojo { /** * output directory where the the netbeans application will be created. * @parameter default-value="${project.build.directory}" * @required */ private File outputDirectory; /** * The Maven Project. * * @parameter expression="${project}" * @required * @readonly */ private MavenProject project; /** * The branding token for the application based on NetBeans platform. * @parameter expression="${netbeans.branding.token}" * @required */ protected String brandingToken; /** * Optional path to custom etc/${brandingToken}.conf file. If not defined, * a default template will be used. * @parameter expression="${netbeans.conf.file}" */ private File etcConfFile; /** * Optional path to custom etc/${brandingToken}.clusters file. If not defined, * a default one will be generated. * @parameter expression="${netbeans.clusters.file}" */ private File etcClustersFile; /** * Directory which contains the executables that will be copied to * the final application's bin/ directory. * Please note that the name of the executables shall generally * match the brandingToken parameter. Otherwise the application can be wrongly branded. * @parameter expression="${netbeans.bin.directory}" */ private File binDirectory; /** * If the depending NBM file doesn't contain any application cluster information, * use this value as default location for such module NBMs. * @parameter default-value="extra" * @since 3.2 */ private String defaultCluster; // <editor-fold defaultstate="collapsed" desc="Component parameters"> /** * @component * @readonly */ private ArtifactFactory artifactFactory; /** * @component * @readonly */ private ArtifactResolver artifactResolver; /** * Local maven repository. * * @parameter expression="${localRepository}" * @required * @readonly */ protected ArtifactRepository localRepository; // end of component params custom code folding // </editor-fold> public void execute() throws MojoExecutionException, MojoFailureException { File nbmBuildDirFile = new File( outputDirectory, brandingToken ); if ( !nbmBuildDirFile.exists() ) { nbmBuildDirFile.mkdirs(); } if ( "nbm-application".equals( project.getPackaging() ) ) { Project antProject = registerNbmAntTasks(); Set<String> knownClusters = new HashSet<String>(); Set<String> wrappedBundleCNBs = new HashSet<String>(); Map<Artifact, ExamineManifest> bundles = new HashMap<Artifact, ExamineManifest>(); @SuppressWarnings( "unchecked" ) Set<Artifact> artifacts = project.getArtifacts(); for ( Artifact art : artifacts ) { ArtifactResult res = turnJarToNbmFile( art, artifactFactory, artifactResolver, project, localRepository ); if (res.hasConvertedArtifact()) { art = res.getConvertedArtifact(); } if ( art.getType().equals( "nbm-file" ) ) { JarFile jf = null; try { jf = new JarFile( art.getFile() ); String clusterName = findCluster( jf ); ClusterTuple cluster = processCluster( clusterName, knownClusters, nbmBuildDirFile, art ); if ( cluster.newer ) { getLog().debug( "Copying " + art.getId() + " to cluster " + clusterName ); Enumeration<JarEntry> enu = jf.entries(); //we need to trigger this ant task to generate the update_tracking file. MakeListOfNBM makeTask = (MakeListOfNBM) antProject.createTask( "genlist" ); antProject.setNewProperty( "module.name", art.getFile().getName() ); //TODO antProject.setProperty( "cluster.dir", clusterName ); FileSet set = makeTask.createFileSet(); set.setDir( cluster.location ); makeTask.setOutputfiledir( cluster.location ); while ( enu.hasMoreElements() ) { JarEntry ent = enu.nextElement(); String name = ent.getName(); if ( name.startsWith( "netbeans/" ) ) { //ignore everything else. String path = clusterName + name.substring( "netbeans".length() ); boolean ispack200 = path.endsWith( ".jar.pack.gz" ); if ( ispack200 ) { path = path.replace( ".jar.pack.gz", ".jar" ); } File fl = new File( nbmBuildDirFile, path.replace( "/", File.separator ) ); if ( ent.isDirectory() ) { fl.mkdirs(); } else { String part = name.substring( "netbeans/".length() ); if ( ispack200 ) { part = part.replace( ".jar.pack.gz", ".jar" ); } set.appendIncludes( new String[] { part }); fl.getParentFile().mkdirs(); fl.createNewFile(); BufferedOutputStream outstream = null; try { outstream = new BufferedOutputStream( new FileOutputStream( fl ) ); InputStream instream = jf.getInputStream( ent ); if ( ispack200 ) { Pack200.Unpacker unp = Pack200.newUnpacker(); JarOutputStream jos = new JarOutputStream( outstream ); GZIPInputStream gzip = new GZIPInputStream( instream ); try { unp.unpack( gzip, jos ); } finally { jos.close(); } } else { IOUtil.copy( instream, outstream ); } } finally { IOUtil.close( outstream ); } //now figure which one of the jars is the module jar.. if ( path.endsWith( ".jar" ) && !path.contains( "locale/" ) && !path.contains( "docs/" )) { ExamineManifest ex = new ExamineManifest( getLog() ); ex.setJarFile( fl ); ex.checkFile(); if ( ex.isNetbeansModule() ) { makeTask.setModule( part ); } else if ( ex.isOsgiBundle() ) { wrappedBundleCNBs.add( ex.getModule() ); } } } } } try { makeTask.execute(); } catch ( BuildException e ) { getLog().error( "Cannot Generate update_tracking xml file" ); throw new MojoExecutionException( e.getMessage(), e ); } } } catch ( IOException ex ) { getLog().error( art.getFile().getAbsolutePath(), ex ); } finally { try { jf.close(); } catch ( IOException ex ) { getLog().error( ex ); } } } if (res.isOSGiBundle()) { bundles.put( art, res.getExaminedManifest() ); } } for (Map.Entry<Artifact, ExamineManifest> ent : bundles.entrySet()) { Artifact art = ent.getKey(); ExamineManifest ex = ent.getValue(); String spec = ex.getModule(); if (wrappedBundleCNBs.contains( spec )) { //we already have this one as a wrapped module. getLog().debug( "Not including bundle " + art.getDependencyConflictId() + ". It is already included in a NetBeans module." ); continue; } ClusterTuple cluster = processCluster( defaultCluster, knownClusters, nbmBuildDirFile, art ); if ( cluster.newer ) { getLog().debug( "Copying " + art.getId() + " to cluster " + defaultCluster ); File modules = new File(cluster.location, "modules"); modules.mkdirs(); File config = new File(cluster.location, "config"); File confModules = new File(config, "Modules"); confModules.mkdirs(); File updateTracking = new File(cluster.location, "update_tracking"); updateTracking.mkdirs(); final String cnb = ex.getModule(); final String cnbDashed = cnb.replace( ".", "-"); final File moduleArt = new File(modules, cnbDashed + ".jar" ); //do we need the file in some canotical name pattern? final String specVer = ex.getSpecVersion(); try { FileUtils.copyFile( art.getFile(), moduleArt ); final File moduleConf = new File(confModules, cnbDashed + ".xml"); FileUtils.copyStreamToFile( new InputStreamFacade() { public InputStream getInputStream() throws IOException { return new StringInputStream( createBundleConfigFile(cnb), "UTF-8"); } }, moduleConf); FileUtils.copyStreamToFile( new InputStreamFacade() { public InputStream getInputStream() throws IOException { return new StringInputStream( createBundleUpdateTracking(cnb, moduleArt, moduleConf, specVer), "UTF-8"); } }, new File(updateTracking, cnbDashed + ".xml")); } catch ( IOException exc ) { getLog().error( exc ); } } } getLog().info( "Created NetBeans module cluster(s) at " + nbmBuildDirFile.getAbsoluteFile() ); } else { throw new MojoExecutionException( "This goal only makes sense on project with nbm-application packaging" ); } //in 6.1 the rebuilt modules will be cached if the timestamp is not touched. File[] files = nbmBuildDirFile.listFiles(); for ( int i = 0; i < files.length; i++ ) { if ( files[i].isDirectory() ) { File stamp = new File( files[i], ".lastModified" ); if ( !stamp.exists() ) { try { stamp.createNewFile(); } catch ( IOException ex ) { ex.printStackTrace(); } } stamp.setLastModified( new Date().getTime() ); } } try { createBinEtcDir( nbmBuildDirFile, brandingToken ); } catch ( IOException ex ) { throw new MojoExecutionException( "Cannot process etc folder content creation.", ex ); } } private final static Pattern patt = Pattern.compile( ".*targetcluster=\"([a-zA-Z0-9_\\.\\-]+)\".*", Pattern.DOTALL ); private String findCluster( JarFile jf ) throws MojoFailureException, IOException { ZipEntry entry = jf.getEntry( "Info/info.xml" ); InputStream ins = jf.getInputStream( entry ); String str = IOUtil.toString( ins, "UTF8" ); Matcher m = patt.matcher( str ); if ( !m.matches() ) { getLog().warn( "Cannot find cluster for " + jf.getName() + " Falling back to default value - '" +defaultCluster + "'."); return defaultCluster; } else { return m.group( 1 ); } } /** * * @param buildDir Directory where the platform bundle is built * @param harnessDir "harness" directory of the netbeans installation * @param enabledClusters The names of all enabled clusters * @param defaultOptions Options for the netbeans platform to be placed in config file * @param brandingToken * * @throws java.io.IOException */ private void createBinEtcDir( File buildDir, String brandingToken ) throws IOException, MojoExecutionException { File etcDir = new File( buildDir + File.separator + "etc" ); etcDir.mkdir(); // create app.clusters which contains a list of clusters to include in the application File clusterConf = new File( etcDir + File.separator + brandingToken + ".clusters" ); String clustersString; if ( etcClustersFile != null ) { clustersString = FileUtils.fileRead( etcClustersFile, "UTF-8" ); } else { clusterConf.createNewFile(); StringBuffer buffer = new StringBuffer(); File[] clusters = buildDir.listFiles( new FileFilter() { public boolean accept( File pathname ) { return new File( pathname, ".lastModified" ).exists(); } } ); for ( File cluster : clusters ) { buffer.append( cluster.getName() ); buffer.append( "\n" ); } clustersString = buffer.toString(); } FileUtils.fileWrite( clusterConf.getAbsolutePath(), clustersString ); File confFile = etcConfFile; String str; if ( confFile == null ) { File harnessDir = new File( buildDir, "harness" ); if ( !harnessDir.exists() ) { getLog().debug( "Using fallback app.conf shipping with the nbm-maven-plugin." ); InputStream instream = null; try { instream = getClass().getClassLoader().getResourceAsStream( "harness/etc/app.conf" ); str = IOUtil.toString( instream, "UTF-8" ); } finally { IOUtil.close( instream ); } } else { // app.conf contains default options and other settings confFile = new File( harnessDir.getAbsolutePath() + File.separator + "etc" + File.separator + "app.conf" ); str = FileUtils.fileRead( confFile, "UTF-8" ); } } else { str = FileUtils.fileRead( confFile, "UTF-8" ); } File confDestFile = new File( etcDir.getAbsolutePath() + File.separator + brandingToken + ".conf" ); str = str.replace( "${branding.token}", brandingToken ); FileUtils.fileWrite( confDestFile.getAbsolutePath(), "UTF-8", str ); File destBinDir = new File( buildDir + File.separator + "bin" ); destBinDir.mkdir(); File binDir; File destExeW = new File( destBinDir, brandingToken + "_w.exe" ); File destExe = new File( destBinDir, brandingToken + ".exe" ); File destSh = new File( destBinDir, brandingToken ); if ( binDirectory != null ) { binDir = binDirectory; File[] fls = binDir.listFiles(); if ( fls == null ) { throw new MojoExecutionException( "Parameter 'binDirectory' has to point to an existing folder." ); } for ( File fl : fls ) { String name = fl.getName(); File dest = null; if ( name.endsWith( "_w.exe" ) ) { dest = destExeW; } else if ( name.endsWith( ".exe" ) ) { dest = destExe; } else if ( !name.contains( "." ) || name.endsWith( ".sh" ) ) { dest = destSh; } if ( dest != null && fl.exists() ) //in 6.7 the _w.exe file is no more. { FileUtils.copyFile( fl, dest ); } else { //warn about file not being copied } } } else { File harnessDir = new File( buildDir, "harness" ); if ( harnessDir.exists() ) { binDir = new File( harnessDir.getAbsolutePath() + File.separator + "launchers" ); File exe = new File( binDir, "app.exe" ); FileUtils.copyFile( exe, destExe ); File exew = new File( binDir, "app_w.exe" ); if ( exew.exists() ) //in 6.7 the _w.exe file is no more. { FileUtils.copyFile( exew, destExeW ); } File sh = new File( binDir, "app.sh" ); FileUtils.copyFile( sh, destSh ); } else { getLog().debug( "Using fallback executables shipping with the nbm-maven-plugin. (from 6.9main NetBeans Platform)" ); writeFile( "harness/launchers/app.sh", destSh ); writeFile( "harness/launchers/app.exe", destExe ); } } Project antProject = new Project(); antProject.init(); Chmod chmod = (Chmod) antProject.createTask( "chmod" ); FileSet fs = new FileSet(); fs.setDir( destBinDir ); fs.setIncludes( "*" ); chmod.addFileset( fs ); chmod.setPerm( "755" ); chmod.execute(); } private void writeFile( String path, File destSh ) throws IOException { InputStream instream = null; OutputStream output = null; try { instream = getClass().getClassLoader().getResourceAsStream( path ); destSh.createNewFile(); output = new BufferedOutputStream( new FileOutputStream( destSh ) ); IOUtil.copy( instream, output ); } finally { IOUtil.close( instream ); IOUtil.close( output ); } } private ClusterTuple processCluster( String cluster, Set<String> knownClusters, File nbmBuildDirFile, Artifact art ) { if ( !knownClusters.contains( cluster ) ) { getLog().info( "Processing cluster '" + cluster + "'" ); knownClusters.add( cluster ); } File clusterFile = new File( nbmBuildDirFile, cluster ); boolean newer = false; if ( !clusterFile.exists() ) { clusterFile.mkdir(); newer = true; } else { File stamp = new File( clusterFile, ".lastModified" ); if ( stamp.lastModified() < art.getFile().lastModified() ) { newer = true; } } return new ClusterTuple( clusterFile, newer ); } private class ClusterTuple { final File location; final boolean newer; private ClusterTuple( File clusterFile, boolean newer ) { location = clusterFile; this.newer = newer; } } static String createBundleConfigFile( String cnb ) { return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + "<!DOCTYPE module PUBLIC \"-//NetBeans//DTD Module Status 1.0//EN\"\n" + " \"http://www.netbeans.org/dtds/module-status-1_0.dtd\">\n" + "<module name=\"" + cnb +"\">\n" + " <param name=\"autoload\">true</param>\n" + " <param name=\"eager\">false</param>\n" + //" <param name=\"enabled\">true</param>\n" + " <param name=\"jar\">modules/" + cnb.replace( ".", "-") + ".jar</param>\n" + " <param name=\"reloadable\">false</param>\n" + "</module>\n"; } static String createBundleUpdateTracking( String cnb, File moduleArt, File moduleConf, String specVersion ) throws FileNotFoundException, IOException { return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + "<module codename=\"" + cnb + "\">\n" + " <module_version install_time=\"" + System.currentTimeMillis() + "\" last=\"true\" origin=\"installer\" specification_version=\"" + specVersion + "\">\n" + " <file crc=\"" + crcForFile(moduleConf).getValue() + "\" name=\"config/Modules/" + cnb.replace( ".", "-") + ".xml\"/>\n" + " <file crc=\"" + crcForFile(moduleArt).getValue() + "\" name=\"modules/" + cnb.replace( ".", "-") + ".jar\"/>\n" + " </module_version>\n" + "</module>"; } static CRC32 crcForFile(File inFile) throws FileNotFoundException, IOException { FileInputStream inFileStream = null; CRC32 crc = new CRC32(); try { inFileStream = new FileInputStream(inFile); byte[] array = new byte[(int) inFile.length()]; int len = inFileStream.read(array); if (len != array.length) { throw new IOException("Cannot fully read " + inFile); } crc.update(array); } finally { inFileStream.close(); } return crc; } }