View Javadoc

1   /* ==========================================================================
2    * Copyright Milos Kleint
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.codehaus.mojo.nbm;
18  
19  import com.google.common.collect.Sets;
20  import java.io.*;
21  import java.net.URL;
22  import java.util.ArrayList;
23  import java.util.Arrays;
24  import java.util.Collection;
25  import java.util.Collections;
26  import java.util.Date;
27  import java.util.Enumeration;
28  import java.util.HashMap;
29  import java.util.HashSet;
30  import java.util.Iterator;
31  import java.util.List;
32  import java.util.Map;
33  import java.util.Set;
34  import java.util.jar.JarEntry;
35  import java.util.jar.JarFile;
36  import java.util.jar.JarOutputStream;
37  import java.util.jar.Pack200;
38  import java.util.regex.Matcher;
39  import java.util.regex.Pattern;
40  import java.util.zip.CRC32;
41  import java.util.zip.GZIPInputStream;
42  import java.util.zip.ZipEntry;
43  import java.util.zip.ZipFile;
44  import org.apache.maven.artifact.Artifact;
45  import org.apache.maven.artifact.factory.ArtifactFactory;
46  import org.apache.maven.artifact.repository.ArtifactRepository;
47  import org.apache.maven.artifact.resolver.AbstractArtifactResolutionException;
48  import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
49  import org.apache.maven.artifact.resolver.ArtifactResolutionException;
50  import org.apache.maven.artifact.resolver.ArtifactResolver;
51  import org.apache.maven.plugin.MojoExecutionException;
52  import org.apache.maven.plugin.MojoFailureException;
53  import org.apache.maven.plugin.logging.Log;
54  import org.apache.maven.plugins.annotations.Component;
55  import org.apache.maven.plugins.annotations.LifecyclePhase;
56  import org.apache.maven.plugins.annotations.Mojo;
57  import org.apache.maven.plugins.annotations.Parameter;
58  import org.apache.maven.plugins.annotations.ResolutionScope;
59  import org.apache.maven.project.MavenProject;
60  import org.apache.tools.ant.BuildException;
61  import org.apache.tools.ant.Project;
62  import org.apache.tools.ant.filters.StringInputStream;
63  import org.apache.tools.ant.taskdefs.Chmod;
64  import org.apache.tools.ant.types.FileSet;
65  import org.codehaus.mojo.nbm.utils.ExamineManifest;
66  import org.codehaus.plexus.util.FileUtils;
67  import org.codehaus.plexus.util.IOUtil;
68  import org.codehaus.plexus.util.StringUtils;
69  import org.codehaus.plexus.util.io.InputStreamFacade;
70  import org.netbeans.nbbuild.MakeListOfNBM;
71  
72  /**
73   * Create the NetBeans module clusters/application for the 'nbm-application' packaging
74   * projects
75   *
76   * @author <a href="mailto:mkleint@codehaus.org">Milos Kleint</a>
77   */
78  @Mojo(name="cluster-app", 
79          defaultPhase= LifecyclePhase.PACKAGE, 
80          requiresProject=true, 
81          threadSafe = true,
82          requiresDependencyResolution= ResolutionScope.RUNTIME )
83  public class CreateClusterAppMojo
84      extends AbstractNbmMojo
85  {
86  
87      /**
88       * output directory where the the NetBeans application will be created.
89       */
90      @Parameter(defaultValue="${project.build.directory}", required=true)
91      private File outputDirectory;
92  
93      /**
94       * The Maven Project.
95       */
96      @Parameter(required=true, readonly=true, property="project")
97      private MavenProject project;
98  
99      /**
100      * The branding token for the application based on NetBeans platform.
101      */
102     @Parameter(property="netbeans.branding.token", required=true)
103     protected String brandingToken;
104 
105     /**
106      * Optional path to custom etc/${brandingToken}.conf file. If not defined,
107      * a default template will be used.
108      */
109     @Parameter( property="netbeans.conf.file")
110     private File etcConfFile;
111 
112     /**
113      * Optional path to custom etc/${brandingToken}.clusters file. If not defined,
114      * a default one will be generated.
115      */
116     @Parameter(property="netbeans.clusters.file")
117     private File etcClustersFile;
118 
119     /**
120      * Directory which contains the executables that will be copied to
121      * the final application's bin/ directory.
122      * Please note that the name of the executables shall generally
123      * match the brandingToken parameter. Otherwise the application can be wrongly branded.
124      */
125     @Parameter(property="netbeans.bin.directory")
126     private File binDirectory;
127 
128     /**
129      * If the depending NBM file doesn't contain any application cluster information,
130      * use this value as default location for such module NBMs.
131      * @since 3.2
132      */
133     @Parameter(defaultValue="extra")
134     private String defaultCluster;
135     
136     /**
137      * attempts to verify the integrity of module artifacts making sure that all dependencies are included
138      * and that all required tokens are provided
139      * @since 3.10
140      */
141     @Parameter(defaultValue = "true", property = "netbeans.verify.integrity")
142     private boolean verifyIntegrity;
143     
144     private final Collection<String> defaultPlatformTokens = Arrays.asList( new String[] {
145                     "org.openide.modules.os.Windows",
146                     "org.openide.modules.os.Unix",
147                     "org.openide.modules.os.MacOSX",
148                     "org.openide.modules.os.OS2",
149                     "org.openide.modules.os.PlainUnix",    
150                     "org.openide.modules.os.Linux",
151                     "org.openide.modules.os.Solaris",
152                     "org.openide.modules.ModuleFormat1",
153                     "org.openide.modules.ModuleFormat2",
154                     "org.openide.modules.jre.JavaFX" //MNBMODULE-234
155     });
156 
157 
158     // <editor-fold defaultstate="collapsed" desc="Component parameters">
159     
160     @Component
161     private ArtifactFactory artifactFactory;
162 
163     @Component
164     private ArtifactResolver artifactResolver;
165 
166     /**
167      * Local maven repository.
168      *
169      */
170     @Parameter(required=true, readonly=true, property="localRepository")
171     protected ArtifactRepository localRepository;
172 
173 // end of component params custom code folding
174 // </editor-fold>
175 
176     public void execute()
177         throws MojoExecutionException, MojoFailureException
178     {
179 
180         File nbmBuildDirFile = new File( outputDirectory, brandingToken );
181         if ( !nbmBuildDirFile.exists() )
182         {
183             nbmBuildDirFile.mkdirs();
184         }
185 
186         if ( "nbm-application".equals( project.getPackaging() ) )
187         {
188             Project antProject = registerNbmAntTasks();
189 
190             Set<String> wrappedBundleCNBs = new HashSet<String>(100);
191             Map<String, Set<String>> clusterDependencies = new HashMap<String, Set<String>>();
192             Map<String, Set<String>> clusterModules = new HashMap<String, Set<String>>();
193             
194             //verify integrity
195             Set<String> modulesCNBs = new HashSet<String>(200);
196             Set<String> dependencyCNBs = new HashSet<String>(200);
197             Map<String, Set<String>> dependencyCNBBacktraces = new HashMap<String, Set<String>>(50);
198             Set<String> requireTokens = new HashSet<String>(50);
199             Map<String, Set<String>> requireTokensBacktraces = new HashMap<String, Set<String>>(50);
200             Set<String> provideTokens = new HashSet<String>(50);
201             Set<String> osgiImports = new HashSet<String>(50);
202             Map<String, Set<String>> osgiImportsBacktraces = new HashMap<String, Set<String>>(50);
203             Set<String> osgiExports = new HashSet<String>(50);
204             Set<String> osgiExportsSubs = new HashSet<String>(50); //a way to deal with nb module declaring xxx.** (subpackages) declaration that is consumed by osgi imports
205             
206             List<BundleTuple> bundles = new ArrayList<BundleTuple>();
207 
208             @SuppressWarnings( "unchecked" )
209             Set<Artifact> artifacts = project.getArtifacts();
210             for ( Artifact art : artifacts )
211             {
212                 ArtifactResult res = turnJarToNbmFile( art, artifactFactory, artifactResolver, project, localRepository );
213                 if ( res.hasConvertedArtifact() )
214                 {
215                     art = res.getConvertedArtifact();
216                 }
217 
218                 if ( art.getType().equals( "nbm-file" ) )
219                 {
220                     try
221                     {
222                         JarFile jf = new JarFile( art.getFile() );
223                         try
224                         {
225                             String clusterName = findCluster( jf );                            
226                             ClusterTuple cluster = processCluster( clusterName, nbmBuildDirFile, art );
227                             
228                                 getLog().debug( "Copying " + art.getId() + " to cluster " + clusterName );
229                                 Enumeration<JarEntry> enu = jf.entries();
230 
231                                 // we need to trigger this ant task to generate the update_tracking file.
232                                 MakeListOfNBM makeTask = (MakeListOfNBM) antProject.createTask( "genlist" );
233                                 antProject.setNewProperty( "module.name", art.getFile().getName() ); // TODO
234                                 antProject.setProperty( "cluster.dir", clusterName );
235                                 FileSet set = makeTask.createFileSet();
236                                 set.setDir( cluster.location );
237                                 makeTask.setOutputfiledir( cluster.location );
238                                 String[] executables = null;
239                                 File classpathRoot = null;
240                                 String classPath = null;
241                                 while ( enu.hasMoreElements() )
242                                 {
243                                     JarEntry ent = enu.nextElement();
244                                     String name = ent.getName();
245                                     //MNBMODULE-176
246                                     if (name.equals("Info/executables.list")) {
247                                         if (cluster.newer) {
248                                             InputStream is = jf.getInputStream( ent );
249                                             executables = StringUtils.split( IOUtil.toString( is, "UTF-8" ), "\n");
250                                         }
251                                     }
252                                     else if ( name.startsWith( "netbeans/" ) )
253                                     { // ignore everything else.
254                                         String path = clusterName + name.substring( "netbeans".length() );
255                                         boolean ispack200 = path.endsWith( ".jar.pack.gz" );
256                                         if ( ispack200 )
257                                         {
258                                             path = path.replace( ".jar.pack.gz", ".jar" );
259                                         }
260                                         File fl = new File( nbmBuildDirFile, path.replace( "/", File.separator ) );
261                                         String part = name.substring( "netbeans/".length() );
262                                         if ( ispack200 )
263                                         {
264                                             part = part.replace( ".jar.pack.gz", ".jar" );
265                                         }
266                                         if (cluster.newer) 
267                                         {
268                                             if ( ent.isDirectory() )
269                                             {
270                                                 fl.mkdirs();
271                                             }
272                                             else if ( path.endsWith( ".external" ) ) // MNBMODULE-138
273                                             {
274                                                 InputStream is = jf.getInputStream( ent );
275                                                 try
276                                                 {
277                                                     externalDownload( new File( fl.getParentFile(),
278                                                                                 fl.getName().replaceFirst( "[.]external$",
279                                                                                                            "" ) ), is );
280                                                 }
281                                                 finally
282                                                 {
283                                                     is.close();
284                                                 }
285                                                 //MNBMODULE-192
286                                                 set.appendIncludes( new String[] { name.substring( "netbeans/".length(), name.length() - ".external".length() ) } );
287                                             }
288                                             else
289                                             {
290                                                 set.appendIncludes( new String[] { part } );
291 
292                                                 fl.getParentFile().mkdirs();
293                                                 fl.createNewFile();
294                                                 BufferedOutputStream outstream = null;
295                                                 try
296                                                 {
297                                                     outstream = new BufferedOutputStream( new FileOutputStream( fl ) );
298                                                     InputStream instream = jf.getInputStream( ent );
299                                                     if ( ispack200 )
300                                                     {
301                                                         Pack200.Unpacker unp = Pack200.newUnpacker();
302                                                         JarOutputStream jos = new JarOutputStream( outstream );
303                                                         GZIPInputStream gzip = new GZIPInputStream( instream );
304                                                         try
305                                                         {
306                                                             unp.unpack( gzip, jos );
307                                                         }
308                                                         finally
309                                                         {
310                                                             jos.close();
311                                                         }
312                                                     }
313                                                     else
314                                                     {
315                                                         IOUtil.copy( instream, outstream );
316                                                     }
317                                                 }
318                                                 finally
319                                                 {
320                                                     IOUtil.close( outstream );
321                                                 }
322                                             }
323                                         }
324                                             
325                                             //TODO examine netbeans/config/Modules to see if the module is autoload/eager
326                                             // in verifyIntegrity these could be handled more gracefully than regular modules.
327                                             //eager is simpler, does not need to have module dependencies satisfied.
328                                             //autoload needs checking if any of the other modules declares a dependency on it. if not, also safe to ignore?
329                                             
330                                             
331                                             // now figure which one of the jars is the module jar..
332                                             if ( part.matches("(modules|core|lib)/[^/]+[.]jar") )
333                                             {
334                                                 ExamineManifest ex = new ExamineManifest( getLog() );
335                                                 ex.setJarFile( fl );
336                                                 ex.setPopulateDependencies( true );
337                                                 ex.checkFile();
338                                                 if ( ex.isNetBeansModule() )
339                                                 {
340                                                     makeTask.setModule( part );
341                                                     addToMap(clusterDependencies, clusterName, ex.getDependencyTokens());
342                                                     addToMap(clusterModules, clusterName, Collections.singletonList( ex.getModule() ));
343                                                     if (ex.getClasspath().length() > 0) { //MNBMODULE-220
344                                                         classPath = ex.getClasspath();
345                                                         classpathRoot = fl.getParentFile();
346                                                     }
347                                                 }
348                                                 if (verifyIntegrity) {
349                                                     dependencyCNBs.addAll(ex.getDependencyTokens());
350                                                     modulesCNBs.add(ex.getModule());
351                                                     for (String d : ex.getDependencyTokens()) {
352                                                         addToMap(dependencyCNBBacktraces, d, Collections.singletonList( ex.getModule() ));
353                                                     }
354                                                     if (ex.isNetBeansModule()) {
355                                                         requireTokens.addAll(ex.getNetBeansRequiresTokens());
356                                                         for (String r : ex.getNetBeansRequiresTokens()) {
357                                                             addToMap( requireTokensBacktraces, r, Collections.singletonList( ex.getModule()));
358                                                         }
359                                                         provideTokens.addAll(ex.getNetBeansProvidesTokens());
360                                                         for (String pack : ex.getPackages()) {
361                                                             if (pack.endsWith( ".**")) {
362                                                                 //what to do with subpackages?
363                                                                 pack = pack.substring( 0, pack.length() - ".**".length());
364                                                                 osgiExportsSubs.add( pack );
365                                                             } else if (pack.endsWith( ".*")) {
366                                                                 pack = pack.substring( 0, pack.length() - ".*".length());
367                                                                 osgiExports.add(pack);                                                            
368                                                             }
369                                                         }
370                                                         
371                                                     }
372                                                 }
373                                             }
374                                         }
375                                     }
376                                     if (classPath != null) { //MNBMODULE-220 collect wrappedbundleCNBs, later useful in assignClustersToBundles(), these get removed from list of bundles.
377                                         String[] paths = StringUtils.split( classPath, " ");
378                                         for (String path : paths) {
379                                             path = path.trim();
380                                             File classpathFile = new File(classpathRoot, path);
381                                             if (path.equals("${java.home}/lib/ext/jfxrt.jar")) { //MNBMODULE-228
382                                                 String jhm = System.getProperty("java.home");
383                                                 classpathFile = new File(new File(new File(new File(jhm), "lib"), "ext"), "jfxrt.jar");
384                                                 if (!classpathFile.exists()) {
385                                                     File jdk7 = new File(new File(new File(jhm), "lib"), "jfxrt.jar");
386                                                     if (jdk7.exists()) {
387                                                         classpathFile = jdk7;
388                                                     }
389                                                 }
390                                             }
391                                             if (!classpathFile.isFile()) {
392                                                 getLog().warn( "Could not resolve Class-Path item in " + art.getId() + ", path is:" + path +  ", skipping");
393                                                 continue; //try to guard against future failures
394                                             } 
395                                             ExamineManifest ex = new ExamineManifest( getLog() );
396                                             ex.setJarFile( classpathFile );
397                                             //ex.setPopulateDependencies( true );
398                                             ex.checkFile();
399                                             if (ex.isOsgiBundle()) {
400                                                 wrappedBundleCNBs.add( ex.getModule() );
401                                             }
402                                         }
403                                     }
404                             if ( cluster.newer )
405                             {
406                                 try
407                                 {
408                                     makeTask.execute();
409                                 }
410                                 catch ( BuildException e )
411                                 {
412                                     getLog().error( "Cannot Generate update_tracking XML file from " + art.getFile() );
413                                     throw new MojoExecutionException( e.getMessage(), e );
414                                 }
415 
416                                 if ( executables != null )
417                                 {
418                                     //MNBMODULE-176
419                                     for ( String exec : executables )
420                                     {
421                                         exec = exec.replace( "/", File.separator );
422                                         File execFile = new File( cluster.location, exec );
423                                         if ( execFile.exists() )
424                                         {
425                                             execFile.setExecutable( true, false );
426                                         }
427                                     }
428                                 }
429                             }
430                             
431                         }
432                         finally
433                         {
434                             jf.close();
435                         }
436                     }
437                     catch ( IOException ex )
438                     {
439                         getLog().error( art.getFile().getAbsolutePath(), ex );
440                     }
441                 }
442                 if ( res.isOSGiBundle() )
443                 {
444                     ExamineManifest ex = res.getExaminedManifest();
445                     bundles.add( new BundleTuple( art,  ex) );
446                     if (verifyIntegrity) {
447                         dependencyCNBs.addAll(ex.getDependencyTokens());
448                         for ( String d : ex.getDependencyTokens() )
449                         {
450                             addToMap( dependencyCNBBacktraces, d, Collections.singletonList( ex.getModule() ) );
451                         }
452                         modulesCNBs.add(ex.getModule());
453                         osgiImports.addAll( ex.getOsgiImports());
454                         for ( String d : ex.getOsgiImports() )
455                         {
456                             addToMap( osgiImportsBacktraces, d, Collections.singletonList( ex.getModule() ) );
457                         }
458                         
459                         osgiExports.addAll( ex.getOsgiExports());
460                     }
461                 } 
462             }
463             
464             if (verifyIntegrity) {
465                 if (getLog().isDebugEnabled()) {
466                     getLog().debug( "All found codenamebases:" + Arrays.toString( modulesCNBs.toArray()) );
467                     getLog().debug( "All found OSGI exports:" + Arrays.toString( osgiExports.toArray()) );
468                     getLog().debug( "All found provided tokens:" + Arrays.toString( provideTokens.toArray()) );
469                 }
470                 dependencyCNBs.removeAll( modulesCNBs );
471                 if (modulesCNBs.contains( "org.netbeans.modules.netbinox")) {
472                     dependencyCNBs.remove( "org.eclipse.osgi"); //this is special.
473                 }
474                 osgiImports.removeAll( osgiExports );
475                 Iterator<String> it = osgiImports.iterator();
476                 while (it.hasNext()) {
477                     String s = it.next();
478                     if (s.startsWith( "java.") || s.startsWith( "javax.") || s.startsWith( "sun.") || s.startsWith( "org.xml.sax") || s.startsWith( "org.w3c.dom") || s.startsWith( "org.ietf.jgss")) {
479                         it.remove();
480                         continue;
481                     }
482                     for (String sub : osgiExportsSubs) {
483                         if (s.startsWith( sub )) {
484                             it.remove();
485                             break;
486                         }
487                     }
488                 }
489                 requireTokens.removeAll( provideTokens );
490                 requireTokens.removeAll( defaultPlatformTokens );
491                 if (!dependencyCNBs.isEmpty() || !osgiImports.isEmpty() ||!requireTokens.isEmpty()) {
492                     if (!dependencyCNBs.isEmpty()) {
493                         getLog().error( "Some included modules/bundles depend on these codenamebases but they are not included. The application will fail starting up. The missing codenamebases are:" );
494                         for (String s : dependencyCNBs) {
495                             Set<String> back = dependencyCNBBacktraces.get( s );
496                             getLog().error("   " + s + (back != null ? "          ref: " + Arrays.toString( back.toArray()) : ""));
497                         }
498                     }
499                     if (!osgiImports.isEmpty()) {
500                         getLog().error("Some OSGi imports are not satisfied by included bundles' exports. The application will fail starting up. The missing imports are:");
501                         for (String s : osgiImports) {
502                             Set<String> back = osgiImportsBacktraces.get( s );
503                             getLog().error("   " + s + (back != null ? "          ref: " + Arrays.toString( back.toArray()) : ""));
504                         }
505                     }
506                      if (!requireTokens.isEmpty()) {
507                         getLog().error("Some tokens required by included modules are not provided by included modules. The application will fail starting up. The missing tokens are:");
508                         for (String s : requireTokens) {
509                             Set<String> back = requireTokensBacktraces.get( s );
510                             getLog().error("   " + s + (back != null ? "          ref: " + Arrays.toString( back.toArray()) : ""));
511                         }
512                     }
513                     throw new MojoFailureException("See above for consistency validation check failures. Either fix those by adding the relevant dependencies to the application or disable the check by setting the verifyIntegrity parameter to false or by running with -Dnetbeans.verify.integrity=false cmd line parameter.");
514                 } else {
515                     getLog().info( "Integrity verification passed.");
516                 }
517             } else {
518                 getLog().info( "Integrity verification skipped.");
519             }
520             
521             //attempt to sort clusters based on the dependencies and cluster content.
522             Map<String, Set<String>> cluster2depClusters = computeClusterOrdering( clusterDependencies, clusterModules );
523             clusterModules.clear();
524         
525             //now assign the cluster to bundles based on dependencies..
526             assignClustersToBundles( bundles, wrappedBundleCNBs, clusterDependencies, cluster2depClusters, getLog() );
527             
528             
529             for (BundleTuple ent : bundles) {
530                 Artifact art = ent.artifact;
531                 final ExamineManifest ex = ent.manifest;
532                 
533                 String clstr = ent.cluster;
534                 if (clstr == null) {
535                     clstr = defaultCluster;
536                 }
537                 
538                 ClusterTuple cluster = processCluster( clstr, nbmBuildDirFile, art );
539                 if ( cluster.newer )
540                 {
541                     getLog().info( "Copying " + art.getId() + " to cluster " + clstr );
542                     File modules = new File( cluster.location, "modules" );
543                     modules.mkdirs();
544                     File config = new File( cluster.location, "config" );
545                     File confModules = new File( config, "Modules" );
546                     confModules.mkdirs();
547                     File updateTracking = new File( cluster.location, "update_tracking" );
548                     updateTracking.mkdirs();
549                     final String cnb = ex.getModule();
550                     final String cnbDashed = cnb.replace( ".", "-" );
551                     final File moduleArt = new File( modules, cnbDashed + ".jar" ); //do we need the file in some canotical name pattern?
552                     final String specVer = ex.getSpecVersion();
553                     try
554                     {
555                         FileUtils.copyFile( art.getFile(), moduleArt );
556                         final File moduleConf = new File( confModules, cnbDashed + ".xml" );
557                         FileUtils.copyStreamToFile( new InputStreamFacade() {
558                             @Override
559                             public InputStream getInputStream() throws IOException
560                             {
561                                 return new StringInputStream( createBundleConfigFile( cnb, ex.isBundleAutoload() ), "UTF-8" );
562                             }
563                         }, moduleConf );
564                         FileUtils.copyStreamToFile( new InputStreamFacade() {
565                             @Override
566                             public InputStream getInputStream() throws IOException
567                             {
568                                 return new StringInputStream( createBundleUpdateTracking( cnb, moduleArt, moduleConf, specVer ), "UTF-8" );
569                             }
570                         }, new File( updateTracking, cnbDashed + ".xml" ) );
571                     }
572                     catch ( IOException exc )
573                     {
574                         getLog().error( exc );
575                     }
576                 }
577             }
578 
579             getLog().info(
580                 "Created NetBeans module cluster(s) at " + nbmBuildDirFile.getAbsoluteFile() );
581 
582         }
583         else
584         {
585             throw new MojoExecutionException(
586                 "This goal only makes sense on project with nbm-application packaging" );
587         }
588         //in 6.1 the rebuilt modules will be cached if the timestamp is not touched.
589         File[] files = nbmBuildDirFile.listFiles();
590         for ( int i = 0; i < files.length; i++ )
591         {
592             if ( files[i].isDirectory() )
593             {
594                 File stamp = new File( files[i], ".lastModified" );
595                 if ( !stamp.exists() )
596                 {
597                     try
598                     {
599                         stamp.createNewFile();
600                     }
601                     catch ( IOException ex )
602                     {
603                         ex.printStackTrace();
604                     }
605                 }
606                 stamp.setLastModified( new Date().getTime() );
607             }
608         }
609         try
610         {
611             createBinEtcDir( nbmBuildDirFile, brandingToken );
612         }
613         catch ( IOException ex )
614         {
615             throw new MojoExecutionException(
616                 "Cannot process etc folder content creation.", ex );
617         }
618     }
619     private final static Pattern patt = Pattern.compile(
620         ".*targetcluster=\"([a-zA-Z0-9_\\.\\-]+)\".*", Pattern.DOTALL );
621 
622     private String findCluster( JarFile jf )
623         throws MojoFailureException, IOException
624     {
625         ZipEntry entry = jf.getEntry( "Info/info.xml" );
626         InputStream ins = jf.getInputStream( entry );
627         String str = IOUtil.toString( ins, "UTF8" );
628         Matcher m = patt.matcher( str );
629         if ( !m.matches() )
630         {
631             getLog().info( "Cannot find cluster for " + jf.getName() + " Falling back to default value - '"
632                                + defaultCluster + "'." );
633             return defaultCluster;
634         }
635         else
636         {
637             return m.group( 1 );
638         }
639     }
640 
641     /**
642      * 
643      * @param buildDir Directory where the platform bundle is built
644      * @param brandingToken
645      * 
646      * @throws java.io.IOException
647      */
648     private void createBinEtcDir( File buildDir, String brandingToken )
649         throws IOException, MojoExecutionException
650     {
651         File etcDir = new File( buildDir + File.separator + "etc" );
652         etcDir.mkdir();
653 
654         // create app.clusters which contains a list of clusters to include in the application
655 
656         File clusterConf = new File( etcDir + File.separator + brandingToken + ".clusters" );
657         String clustersString;
658         if ( etcClustersFile != null )
659         {
660             clustersString = FileUtils.fileRead( etcClustersFile, "UTF-8" );
661         }
662         else
663         {
664             clusterConf.createNewFile();
665             StringBuffer buffer = new StringBuffer();
666             File[] clusters = buildDir.listFiles( new FileFilter()
667             {
668 
669                 @Override
670                 public boolean accept( File pathname )
671                 {
672                     return new File( pathname, ".lastModified" ).exists();
673                 }
674             } );
675             for ( File cluster : clusters )
676             {
677                 buffer.append( cluster.getName() );
678                 buffer.append( "\n" );
679             }
680             clustersString = buffer.toString();
681         }
682 
683         FileUtils.fileWrite( clusterConf.getAbsolutePath(), clustersString );
684 
685         File confFile = etcConfFile;
686         String str;
687         if ( confFile == null )
688         {
689             File harnessDir = new File( buildDir, "harness" );
690             // app.conf contains default options and other settings
691             confFile = new File(
692                     harnessDir.getAbsolutePath() + File.separator + "etc" + File.separator + "app.conf" );
693             if ( confFile.exists() )
694             {
695                 str = FileUtils.fileRead( confFile, "UTF-8" );
696             }
697             else 
698             {
699                 getLog().debug( "Using fallback app.conf shipping with the nbm-maven-plugin." );
700                 InputStream instream = null;
701                 try
702                 {
703                     instream = getClass().getClassLoader().getResourceAsStream( "harness/etc/app.conf" );
704                     str = IOUtil.toString( instream, "UTF-8" );
705                 }
706                 finally
707                 {
708                     IOUtil.close( instream );
709                 }
710             }
711         }
712         else
713         {
714             str = FileUtils.fileRead( confFile, "UTF-8" );
715         }
716         File confDestFile = new File(
717             etcDir.getAbsolutePath() + File.separator + brandingToken + ".conf" );
718 
719         str = str.replace( "${branding.token}", brandingToken );
720         FileUtils.fileWrite( confDestFile.getAbsolutePath(), "UTF-8", str );
721 
722         File destBinDir = new File( buildDir + File.separator + "bin" );
723         destBinDir.mkdir();
724 
725         File binDir;
726         File destExeW = new File( destBinDir, brandingToken + "_w.exe" );
727         File destExe = new File( destBinDir, brandingToken + ".exe" );
728         File destExe64 = new File( destBinDir, brandingToken + "64.exe" );
729         File destSh = new File( destBinDir, brandingToken );
730 
731         if ( binDirectory != null )
732         {
733             //we have custom launchers.
734             binDir = binDirectory;
735             File[] fls = binDir.listFiles();
736             if ( fls == null )
737             {
738                 throw new MojoExecutionException( "Parameter 'binDirectory' has to point to an existing folder." );
739             }
740             for ( File fl : fls )
741             {
742                 String name = fl.getName();
743                 File dest = null;
744                 if ( name.endsWith( "_w.exe" ) ) 
745                 {
746                     dest = destExeW;
747                 }
748                 else if ( name.endsWith( "64.exe" ) )
749                 {
750                     dest = destExe64;
751                 }
752                 else if ( name.endsWith( ".exe" ) )
753                 {
754                     dest = destExe;
755                 }
756                 else if ( !name.contains( "." ) || name.endsWith( ".sh" ) )
757                 {
758                     dest = destSh;
759                 }
760                 if ( dest != null  && fl.exists() ) //in 6.7 the _w.exe file is no more.
761                 {
762                     FileUtils.copyFile( fl, dest );
763                 }
764                 else
765                 {
766                     //warn about file not being copied
767                 }
768             }
769         }
770         else
771         {
772             File harnessDir = new File( buildDir, "harness" );
773             //we have org-netbeans-modules-apisupport-harness in target area, just use it's own launchers.
774             binDir = new File(
775                     harnessDir.getAbsolutePath() + File.separator + "launchers" );
776             if ( binDir.exists() )
777             {
778                 File exe = new File( binDir, "app.exe" );
779                 FileUtils.copyFile( exe, destExe );
780                 File exe64 = new File( binDir, "app64.exe" );
781                 if ( exe64.isFile() )
782                 {
783                     FileUtils.copyFile( exe64, destExe64 );
784                 }
785                 File exew = new File( binDir, "app_w.exe" );
786                 if ( exew.exists() ) //in 6.7 the _w.exe file is no more.
787                 {
788                     FileUtils.copyFile( exew, destExeW );
789                 }
790                 File sh = new File( binDir, "app.sh" );
791                 FileUtils.copyFile( sh, destSh );
792             }
793             else
794             {
795                 File nbm = getHarnessNbm();
796                 ZipFile zip = new ZipFile( nbm );
797                 try {
798                     getLog().debug( "Using fallback executables from downloaded org-netbeans-modules-apisupport-harness nbm file." );
799                     writeFromZip(zip, "netbeans/launchers/app.sh",  destSh, true );
800                     writeFromZip(zip, "netbeans/launchers/app.exe",  destExe, true );
801                     writeFromZip(zip, "netbeans/launchers/app64.exe",  destExe64, false );
802                     writeFromZip(zip, "netbeans/launchers/app_w.exe",  destExeW, false );
803                 } finally {
804                     zip.close();
805                 }
806             }
807         }
808 
809         Project antProject = antProject();
810 
811         Chmod chmod = (Chmod) antProject.createTask( "chmod" );
812         FileSet fs = new FileSet();
813         fs.setDir( destBinDir );
814         fs.setIncludes( "*" );
815         chmod.addFileset( fs );
816         chmod.setPerm( "755" );
817         chmod.execute();
818     }
819 
820     private void writeFile( String path, File destSh )
821         throws IOException
822     {
823         InputStream instream = null;
824         OutputStream output = null;
825         try
826         {
827             instream = getClass().getClassLoader().getResourceAsStream( path );
828             if ( instream == null )
829             {
830                 throw new FileNotFoundException( path );
831             }
832             destSh.createNewFile();
833             output = new BufferedOutputStream( new FileOutputStream( destSh ) );
834             IOUtil.copy( instream, output );
835         }
836         finally
837         {
838             IOUtil.close( instream );
839             IOUtil.close( output );
840         }
841     }
842 
843     private ClusterTuple processCluster( String cluster, File nbmBuildDirFile, Artifact art )
844     {
845         File clusterFile = new File( nbmBuildDirFile, cluster );
846         boolean newer = false;
847         if ( !clusterFile.exists() )
848         {
849             clusterFile.mkdir();
850             newer = true;
851         }
852         else
853         {
854             File stamp = new File( clusterFile, ".lastModified" );
855             if ( stamp.lastModified() < art.getFile().lastModified() )
856             {
857                 newer = true;
858             }
859         }
860         return new ClusterTuple( clusterFile, newer );
861     }
862 
863     private void externalDownload( File f, InputStream is )
864         throws IOException
865     {
866         // Cf. org.netbeans.nbbuild.AutoUpdate
867         BufferedReader r = new BufferedReader( new InputStreamReader( is, "UTF-8" ) );
868         long crc = -1;
869         long size = -1;
870         boolean found = false;
871         String line;
872         while ( ( line = r.readLine() ) != null )
873         {
874             if ( line.startsWith( "CRC:" ) )
875             {
876                 crc = Long.parseLong( line.substring( 4 ).trim() );
877             }
878             else if ( line.startsWith( "URL:m2:/" ) )
879             {
880                 if ( ! found )
881                 {
882                     String[] coords = line.substring( 8 ).trim().split( ":" );
883                     Artifact artifact;
884                     if ( coords.length == 4 )
885                     {
886                         artifact = artifactFactory.createArtifact( coords[0], coords[1], coords[2], null, coords[3] );
887                     }
888                     else
889                     {
890                         artifact = artifactFactory.createArtifactWithClassifier( coords[0], coords[1], coords[2], coords[3], coords[4] );
891                     }
892                     try
893                     {
894                         artifactResolver.resolve( artifact, project.getRemoteArtifactRepositories(), localRepository );
895                         FileUtils.copyFile( artifact.getFile(), f );
896                         found = true;
897                     }
898                     catch ( AbstractArtifactResolutionException x )
899                     {
900                         getLog().warn( "Cannot find " + line.substring( 8 ), x );
901                     }
902                 }
903             }
904             else if ( line.startsWith( "URL:" ) )
905             {
906                 if ( ! found )
907                 {
908                     String url = line.substring( 4 ).trim();
909                     try
910                     {
911                         // XXX use Wagon API instead
912                         FileUtils.copyURLToFile( new URL( url ), f );
913                         found = true;
914                     }
915                     catch ( IOException x )
916                     {
917                         getLog().warn( "Cannot download " + url, x );
918                     }
919                 }
920             }
921             else if ( line.startsWith( "SIZE:" ) )
922             {
923                 size = Long.parseLong( line.substring( 5 ).trim() );
924             }
925             else
926             {
927                 getLog().warn( "Unrecognized line: " + line );
928             }
929         }
930         if ( ! found )
931         {
932             throw new IOException( "Could not download " + f );
933         }
934         if ( crc != -1 && crc != crcForFile( f ).getValue() )
935         {
936             throw new IOException( "CRC-32 of " + f + " does not match declared " + crc );
937         }
938         if ( size != -1 && size != f.length() )
939         {
940             throw new IOException( "Size of " + f + " does not match declared " + size );
941         }
942     }
943 
944     private File getHarnessNbm() throws MojoExecutionException
945     {
946         @SuppressWarnings( "unchecked" )
947         Set<Artifact> artifacts = project.getArtifacts();
948         String version = null;
949         for (Artifact a : artifacts) {
950             if ("org.netbeans.modules".equals(a.getGroupId()) && "org-netbeans-bootstrap".equals(a.getArtifactId())) {
951                 version = a.getBaseVersion(); //base version in non-snapshot should equals version, in snapshots to X-SNAPSHOT, not timestamp
952                 break;
953             }
954         }
955         if (version == null) {
956             throw new MojoExecutionException( "We could not find org-netbeans-bootstrap among the modules in the application. Launchers could not be found.");
957         }
958         Artifact nbmArt = artifactFactory.createArtifact(
959             "org.netbeans.modules",
960             "org-netbeans-modules-apisupport-harness",
961             version,
962             "compile",
963             "nbm-file");
964         try
965         {
966             artifactResolver.resolve( nbmArt, project.getRemoteArtifactRepositories(), localRepository );
967         }
968 
969         catch ( ArtifactResolutionException ex )
970         {
971             throw new MojoExecutionException( "Failed to retrieve the nbm file from repository", ex );
972         }
973         catch ( ArtifactNotFoundException ex )
974         {
975             throw new MojoExecutionException( "Failed to retrieve the nbm file from repository", ex );
976         }
977         return nbmArt.getFile();
978     }
979 
980     private void writeFromZip( final ZipFile zip, String zipPath, File destFile, boolean mandatory ) throws MojoExecutionException, IOException
981     {
982         final ZipEntry path = zip.getEntry( zipPath );
983         if (path == null) {
984             if (mandatory) {
985                 throw new MojoExecutionException( zipPath + " not found in " + zip.getName());
986             }
987             getLog().debug(zipPath + " is not present in " + zip.getName());
988             return;
989         }
990         FileUtils.copyStreamToFile( new InputStreamFacade() {
991             
992             @Override
993             public InputStream getInputStream() throws IOException
994             {
995                 return zip.getInputStream( path );
996             }
997         }, destFile);
998     }
999 
1000     private static void addToMap( Map<String, Set<String>> map, String clusterName, List<String> newValues )
1001     {
1002         Set<String> lst = map.get( clusterName );
1003         if ( lst == null )
1004         {
1005             lst = new HashSet<String>();
1006             map.put( clusterName, lst );
1007         }
1008         if ( newValues != null )
1009         {
1010             lst.addAll( newValues );
1011         }
1012     }
1013     
1014     private static List<String> findByDependencies( Map<String, Set<String>> clusterDependencies, String spec)
1015     {
1016         List<String> toRet = new ArrayList<String>();
1017         for ( Map.Entry<String, Set<String>> entry : clusterDependencies.entrySet() )
1018         {
1019             if ( entry.getValue().contains( spec ) )
1020             {
1021                 toRet.add(entry.getKey());
1022             }
1023         }
1024         return toRet;
1025     }
1026 
1027     //the basic idea is that bundle's cluster can be determined by who depends on it.
1028     //simplest case is when a module depends on it. If there are more, we need to pick one that is "lower in the stack, that's what cluster2depClusters is for.
1029     //the rest needs to be determined in more sofisticated manner.
1030     //start from bundles with known cluster and see what other bundles they depend on. stamp all these with the same cluster. do it recursively.
1031     //At the end process the remaining bundles in reverse order. Check if *they* depend on a bundle with known cluster and so on..
1032     //A few unsolved cases:
1033     // - we never update the cluster information once a match was found, but there is a possibility that later in the processing the cluster could be "lowered".
1034     // - 2 or more modules from unrelated clusters we cannot easily decide, most likely should be in common denominator cluster but our cluster2depClusters map is not transitive, only lists direct dependencies
1035     static void assignClustersToBundles( List<BundleTuple> bundles, Set<String> wrappedBundleCNBs, Map<String, Set<String>> clusterDependencies, Map<String, Set<String>> cluster2depClusters, Log log)
1036     {
1037         List<BundleTuple> toProcess = new ArrayList<BundleTuple>();
1038         List<BundleTuple> known = new ArrayList<BundleTuple>();
1039         for ( Iterator<BundleTuple> it = bundles.iterator(); it.hasNext(); )
1040         {
1041             BundleTuple ent = it.next();
1042             Artifact art = ent.artifact;
1043             ExamineManifest ex = ent.manifest;
1044             String spec = ex.getModule();
1045             if ( wrappedBundleCNBs.contains( spec ) )
1046             {
1047                 // we already have this one as a wrapped module.
1048                 log.debug( "Not including bundle " + art.getDependencyConflictId()
1049                                     + ". It is already included in a NetBeans module" );
1050                 it.remove();
1051                 continue;
1052             }
1053             List<String> depclusters = findByDependencies(clusterDependencies, spec);
1054             if (depclusters.size() == 1) {
1055                 ent.cluster = depclusters.get( 0 );
1056                 known.add( ent );
1057             } else if (depclusters.isEmpty()) {
1058                 toProcess.add(ent);
1059             } else {
1060                 //more results.. from 2 dependent clusters pick the one that is lower in the stack.
1061                 for ( Iterator<String> it2 = depclusters.iterator(); it2.hasNext(); )
1062                 {
1063                     String s = it2.next();
1064                     Set<String> depsCs = cluster2depClusters.get( s );
1065                     boolean removeS = false;
1066                     for (String sDep : depclusters) {
1067                         if (s.equals( sDep) ) {
1068                             continue;
1069                         }
1070                         if (depsCs != null && depsCs.contains( sDep ) ) {
1071                             removeS = true;
1072                         }
1073                     }
1074                     if (removeS) {
1075                         it2.remove();
1076                     }
1077                 }
1078                 ent.cluster = depclusters.get( 0 ); //TODO still some free room there, what if they don't directly depend on each other but still are related
1079                 known.add (ent);
1080             }
1081         }
1082         if (!toProcess.isEmpty())
1083         {
1084             walkKnownBundleDependenciesDown(known, toProcess);
1085         }
1086         if (!toProcess.isEmpty())
1087         {
1088             walkKnownBundleDependenciesUp(known, toProcess);
1089         }
1090     }
1091 
1092     private static void walkKnownBundleDependenciesDown( List<BundleTuple> known, List<BundleTuple> toProcess )
1093     {
1094         boolean atLeastOneWasFound = false;
1095         for ( Iterator<BundleTuple> it = toProcess.iterator(); it.hasNext(); )
1096         {
1097             BundleTuple bundleTuple = it.next();
1098             boolean found = false;
1099             for ( BundleTuple knownBT : known)
1100             {
1101                 Sets.SetView<String> is = Sets.intersection(bundleTuple.manifest.getOsgiExports() , knownBT.manifest.getOsgiImports() );
1102                 if (!is.isEmpty()) {
1103                     found = true;
1104                     bundleTuple.cluster = knownBT.cluster;
1105                     break;
1106                 }
1107                 //dependencyTokens are requireBundle - matches the module property
1108                 is = Sets.intersection(Collections.singleton( bundleTuple.manifest.getModule()), new HashSet(knownBT.manifest.getDependencyTokens()) );
1109                 if (!is.isEmpty()) {
1110                     found = true;
1111                     bundleTuple.cluster = knownBT.cluster;
1112                     break;
1113                 }
1114                 
1115             }
1116             if (found) {
1117                 atLeastOneWasFound = true;
1118                 it.remove();
1119                 known.add(bundleTuple);
1120             }
1121             
1122         }
1123         if (!toProcess.isEmpty() && atLeastOneWasFound) {
1124             walkKnownBundleDependenciesDown( known, toProcess );
1125         }
1126     }
1127 
1128     private static void walkKnownBundleDependenciesUp( List<BundleTuple> known, List<BundleTuple> toProcess )
1129     {
1130         boolean atLeastOneWasFound = false;
1131         for ( Iterator<BundleTuple> it = toProcess.iterator(); it.hasNext(); )
1132         {
1133             BundleTuple bundleTuple = it.next();
1134             boolean found = false;
1135             for ( BundleTuple knownBT : known)
1136             {
1137                 Sets.SetView<String> is = Sets.intersection(bundleTuple.manifest.getOsgiImports() , knownBT.manifest.getOsgiExports() );
1138                 if (!is.isEmpty()) {
1139                     found = true;
1140                     bundleTuple.cluster = knownBT.cluster;
1141                     break;
1142                 }
1143                 //dependencyTokens are requireBundle - matches the module property
1144                 is = Sets.intersection(Collections.singleton( knownBT.manifest.getModule()), new HashSet(bundleTuple.manifest.getDependencyTokens()) );
1145                 if (!is.isEmpty()) {
1146                     found = true;
1147                     bundleTuple.cluster = knownBT.cluster;
1148                     break;
1149                 }
1150                 
1151             }
1152             if (found) {
1153                 atLeastOneWasFound = true;
1154                 it.remove();
1155                 known.add(bundleTuple);
1156             }
1157             
1158         }
1159         if (!toProcess.isEmpty() && atLeastOneWasFound) {
1160             walkKnownBundleDependenciesDown( known, toProcess );
1161         }
1162         if (!toProcess.isEmpty() && atLeastOneWasFound) {
1163             walkKnownBundleDependenciesUp( known, toProcess );
1164         }
1165     }
1166 
1167     //static and default for tests..
1168     static Map<String, Set<String>> computeClusterOrdering( Map<String, Set<String>> clusterDependencies, Map<String, Set<String>> clusterModules )
1169     {
1170         Map<String, Set<String>> cluster2depClusters = new HashMap<String, Set<String>>();
1171         for ( Map.Entry<String, Set<String>> entry : clusterDependencies.entrySet() )
1172         {
1173             String cluster = entry.getKey();
1174             Set<String> deps = entry.getValue();
1175             for (Map.Entry<String, Set<String>> subEnt : clusterModules.entrySet()) {
1176                 if (subEnt.getKey().equals( cluster) ) {
1177                     continue;
1178                 }
1179                 Sets.SetView<String> is = Sets.intersection(subEnt.getValue(), deps );
1180                 if (!is.isEmpty()) {
1181                     addToMap( cluster2depClusters, cluster, Collections.singletonList( subEnt.getKey() ) );
1182                 }
1183             }
1184         }
1185         return cluster2depClusters;
1186     }
1187     
1188     static class BundleTuple {
1189         final Artifact artifact;
1190         final ExamineManifest manifest;
1191         String cluster;
1192 
1193         BundleTuple( Artifact artifact, ExamineManifest manifest )
1194         {
1195             this.artifact = artifact;
1196             this.manifest = manifest;
1197         }
1198         
1199     }
1200 
1201     private static class ClusterTuple
1202     {
1203         final File location;
1204         final boolean newer;
1205 
1206         private ClusterTuple( File clusterFile, boolean newer )
1207         {
1208             location = clusterFile;
1209             this.newer = newer;
1210         }
1211     }
1212 
1213     static String createBundleConfigFile( String cnb, boolean autoload)
1214     {
1215         return
1216 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
1217 "<!DOCTYPE module PUBLIC \"-//NetBeans//DTD Module Status 1.0//EN\"\n" +
1218 "                        \"http://www.netbeans.org/dtds/module-status-1_0.dtd\">\n" +
1219 "<module name=\"" + cnb +"\">\n" +
1220 "    <param name=\"autoload\">" + autoload + "</param>\n" +
1221 "    <param name=\"eager\">false</param>\n" + (autoload ? "" : "    <param name=\"enabled\">true</param>\n") +
1222 "    <param name=\"jar\">modules/" + cnb.replace( ".", "-") + ".jar</param>\n" +
1223 "    <param name=\"reloadable\">false</param>\n" +
1224 "</module>\n";
1225     }
1226 
1227     static String createBundleUpdateTracking( String cnb, File moduleArt, File moduleConf, String specVersion )
1228         throws FileNotFoundException, IOException
1229     {
1230 
1231         return
1232 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
1233 "<module codename=\"" + cnb + "\">\n" +
1234 "    <module_version install_time=\"" + System.currentTimeMillis() + "\" last=\"true\" origin=\"installer\" specification_version=\"" + specVersion + "\">\n" +
1235 "        <file crc=\"" + crcForFile( moduleConf ).getValue() + "\" name=\"config/Modules/" + cnb.replace( ".", "-" ) + ".xml\"/>\n" +
1236 "        <file crc=\"" + crcForFile( moduleArt ).getValue() + "\" name=\"modules/" + cnb.replace( ".", "-" ) + ".jar\"/>\n" +
1237 "    </module_version>\n" +
1238 "</module>";
1239 
1240     }
1241 
1242     static CRC32 crcForFile( File inFile )
1243         throws FileNotFoundException, IOException
1244     {
1245         CRC32 crc = new CRC32();
1246         InputStream inFileStream = new FileInputStream( inFile );
1247         try {
1248             byte[] array = new byte[(int) inFile.length()];
1249             int len = inFileStream.read( array );
1250             if ( len != array.length )
1251             {
1252                 throw new IOException( "Cannot fully read " + inFile );
1253             }
1254             crc.update( array );
1255         }
1256         finally
1257         {
1258             inFileStream.close();
1259         }
1260 
1261         return crc;
1262     }
1263 
1264 }