View Javadoc

1   /*
2    *  Copyright 2008 Johan Andrén.
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   *  under the License.
16   */
17  package org.codehaus.mojo.nbm;
18  
19  import java.io.BufferedReader;
20  import java.io.ByteArrayInputStream;
21  import java.io.File;
22  import java.io.FileInputStream;
23  import java.io.FileOutputStream;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.InputStreamReader;
27  import java.io.OutputStreamWriter;
28  import java.io.Reader;
29  import java.io.Writer;
30  import java.net.URL;
31  import java.util.Properties;
32  import java.util.StringTokenizer;
33  import java.util.jar.JarFile;
34  import org.apache.maven.plugin.MojoExecutionException;
35  import org.apache.maven.plugin.MojoFailureException;
36  import org.apache.maven.plugins.annotations.Component;
37  import org.apache.maven.plugins.annotations.LifecyclePhase;
38  import org.apache.maven.plugins.annotations.Mojo;
39  import org.apache.maven.project.MavenProject;
40  import org.apache.maven.project.MavenProjectHelper;
41  import org.apache.tools.ant.Project;
42  import org.apache.tools.ant.taskdefs.GenerateKey;
43  import org.apache.tools.ant.taskdefs.SignJar;
44  import org.apache.tools.ant.taskdefs.Taskdef;
45  import org.apache.tools.ant.types.FileSet;
46  import org.apache.tools.ant.types.Parameter;
47  import org.apache.tools.ant.types.selectors.AndSelector;
48  import org.apache.tools.ant.types.selectors.FilenameSelector;
49  import org.apache.tools.ant.types.selectors.OrSelector;
50  import org.codehaus.plexus.archiver.zip.ZipArchiver;
51  import org.codehaus.plexus.components.io.resources.PlexusIoResource;
52  import org.codehaus.plexus.util.DirectoryScanner;
53  import org.codehaus.plexus.util.FileUtils;
54  import org.codehaus.plexus.util.IOUtil;
55  import org.codehaus.plexus.util.InterpolationFilterReader;
56  import org.netbeans.nbbuild.MakeJNLP;
57  import org.netbeans.nbbuild.ModuleSelector;
58  import org.netbeans.nbbuild.VerifyJNLP;
59  
60  /**
61   * Create webstartable binaries for a 'nbm-application'.
62   * @author <a href="mailto:johan.andren@databyran.se">Johan Andrén</a>
63   * @author <a href="mailto:mkleint@codehaus.org">Milos Kleint</a>
64   * @since 3.0
65   */
66  @Mojo(name="webstart-app", defaultPhase= LifecyclePhase.PACKAGE )
67  public class CreateWebstartAppMojo
68      extends AbstractNbmMojo
69  {
70  
71      /**
72       * The Maven project.
73  
74       */
75      @org.apache.maven.plugins.annotations.Parameter(required=true, readonly=true, property="project")
76      private MavenProject project;
77  
78      @Component
79      protected MavenProjectHelper projectHelper;
80  
81      /**
82       * The branding token for the application based on NetBeans platform.
83       */
84      @org.apache.maven.plugins.annotations.Parameter(required=true, property="netbeans.branding.token")
85      protected String brandingToken;
86  
87      /**
88       * output directory where the the NetBeans application will be created.
89       */
90      @org.apache.maven.plugins.annotations.Parameter(required=true, defaultValue="${project.build.directory}")
91      private File outputDirectory;
92  
93      /**
94       * Ready-to-deploy WAR containing application in JNLP packaging.
95       * 
96       */
97      @org.apache.maven.plugins.annotations.Parameter(required=true, defaultValue="${project.build.directory}/${project.artifactId}-${project.version}-jnlp.war")
98      private File destinationFile;
99  
100     /**
101      * Artifact Classifier to use for the webstart distributable zip file.
102      * @since 3.1
103      */
104     @org.apache.maven.plugins.annotations.Parameter(defaultValue="webstart", property="nbm.webstart.classifier")
105     private String webstartClassifier;
106 
107     /**
108      * Codebase value within *.jnlp files.
109      * <strong>Defining this parameter is generally a bad idea.</strong>
110      */
111     @org.apache.maven.plugins.annotations.Parameter(property="nbm.webstart.codebase")
112     private String codebase;
113 
114     /**
115      * A custom master JNLP file. If not defined, the 
116      * <a href="http://mojo.codehaus.org/nbm-maven-plugin/masterjnlp.txt">default one</a> is used.
117      * The following expressions can be used within the file and will
118      * be replaced when generating content.
119      * <ul>
120      * <li>${jnlp.resources}</li>
121      * <li>${jnlp.codebase} - the 'codebase' parameter value is passed in.</li>
122      * <li>${app.name}</li>
123      * <li>${app.title}</li>
124      * <li>${app.vendor}</li>
125      * <li>${app.description}</li>
126      * <li>${branding.token} - the 'brandingToken' parameter value is passed in.</li>
127      * <li>${netbeans.jnlp.fixPolicy}</li>
128      * </ul>
129      */
130     @org.apache.maven.plugins.annotations.Parameter
131     private File masterJnlpFile;
132     
133     /**
134      * The basename (minus .jnlp extension) of the master JNLP file in the output.
135      * This file will be the entry point for javaws.
136      * Defaults to the branding token.
137      * @since 3.5
138      */
139     @org.apache.maven.plugins.annotations.Parameter(property="master.jnlp.file.name")
140     private String masterJnlpFileName;
141 
142     /**
143      * keystore location for signing the nbm file
144      */
145     @org.apache.maven.plugins.annotations.Parameter(property="keystore")
146     private String keystore;
147 
148     /**
149      * keystore password
150      */
151     @org.apache.maven.plugins.annotations.Parameter(property="keystorepass")
152     private String keystorepassword;
153 
154     /**
155      * keystore alias
156      */
157     @org.apache.maven.plugins.annotations.Parameter(property="keystorealias")
158     private String keystorealias;
159 
160     /**
161      * keystore type
162      * @since 3.5
163      */
164     @org.apache.maven.plugins.annotations.Parameter(property="keystoretype")
165     private String keystoretype;
166 
167     /**
168      * If set true, build-jnlp target creates versioning info in jnlp descriptors and version.xml files.
169      * This allows for incremental updates of Webstart applications, but requires download via
170      * JnlpDownloadServlet
171      * Defaults to false, which means versioning
172      * info is not generated (see
173      * http://java.sun.com/j2se/1.5.0/docs/guide/javaws/developersguide/downloadservletguide.html#resources).
174      *
175      */
176     @org.apache.maven.plugins.annotations.Parameter(defaultValue="false", property="nbm.webstart.versions")
177     private boolean processJarVersions;
178     /**
179      * additional command line arguments. Eg.
180      * -J-Xdebug -J-Xnoagent -J-Xrunjdwp:transport=dt_socket,suspend=n,server=n,address=8888
181      * can be used to debug the IDE.
182      */
183     @org.apache.maven.plugins.annotations.Parameter(property="netbeans.run.params")
184     private String additionalArguments;
185 
186     /**
187      * 
188      * @throws org.apache.maven.plugin.MojoExecutionException 
189      * @throws org.apache.maven.plugin.MojoFailureException 
190      */
191     @Override
192     public void execute()
193         throws MojoExecutionException, MojoFailureException
194     {
195         if ( !"nbm-application".equals( project.getPackaging() ) )
196         {
197             throw new MojoExecutionException(
198                 "This goal only makes sense on project with nbm-application packaging." );
199         }
200         Project antProject = antProject();
201         
202         getLog().warn( "WARNING: Unsigned and self-signed WebStart applications are deprecated from JDK7u21 onwards. To ensure future correct functionality please use trusted certificate.");
203 
204         if ( keystore != null && keystorealias != null && keystorepassword != null )
205         {
206             File ks = new File( keystore );
207             if ( !ks.exists() )
208             {
209                 throw new MojoFailureException( "Cannot find keystore file at " + ks.getAbsolutePath() );
210             }
211             else
212             {
213                 //proceed..
214             }
215         }
216         else if ( keystore != null || keystorepassword != null || keystorealias != null )
217         {
218             throw new MojoFailureException(
219                 "If you want to sign the jnlp application, you need to define all three keystore related parameters." );
220         }
221         else
222         {
223             File generatedKeystore = new File( outputDirectory, "generated.keystore" );
224             if ( ! generatedKeystore.exists() )
225             {
226                 getLog().warn( "Keystore related parameters not set, generating a default keystore." );
227                 GenerateKey genTask = (GenerateKey) antProject.createTask( "genkey" );
228                 genTask.setAlias( "jnlp" );
229                 genTask.setStorepass( "netbeans" );
230                 genTask.setDname( "CN=" + System.getProperty( "user.name" ) );
231                 genTask.setKeystore( generatedKeystore.getAbsolutePath() );
232                 genTask.execute();
233             }
234             keystore = generatedKeystore.getAbsolutePath();
235             keystorepassword = "netbeans";
236             keystorealias = "jnlp";
237         }
238 
239         Taskdef taskdef = (Taskdef) antProject.createTask( "taskdef" );
240         taskdef.setClassname( "org.netbeans.nbbuild.MakeJNLP" );
241         taskdef.setName( "makejnlp" );
242         taskdef.execute();
243 
244         taskdef = (Taskdef) antProject.createTask( "taskdef" );
245         taskdef.setClassname( "org.netbeans.nbbuild.VerifyJNLP" );
246         taskdef.setName( "verifyjnlp" );
247         taskdef.execute();
248 
249 
250         try
251         {
252             File webstartBuildDir = new File(
253                 outputDirectory + File.separator + "webstart" + File.separator + brandingToken );
254             if ( webstartBuildDir.exists() )
255             {
256                 FileUtils.deleteDirectory( webstartBuildDir );
257             }
258             webstartBuildDir.mkdirs();
259             final String localCodebase = codebase != null ? codebase : webstartBuildDir.toURI().toString();
260             getLog().info( "Generating webstartable binaries at " + webstartBuildDir.getAbsolutePath() );
261 
262             File nbmBuildDirFile = new File( outputDirectory, brandingToken );
263 
264 //            FileUtils.copyDirectoryStructureIfModified( nbmBuildDirFile, webstartBuildDir );
265 
266             MakeJNLP jnlpTask = (MakeJNLP) antProject.createTask( "makejnlp" );
267             jnlpTask.setDir( webstartBuildDir );
268             jnlpTask.setCodebase( localCodebase );
269             //TODO, how to figure verify excludes..
270             jnlpTask.setVerify( false );
271             jnlpTask.setPermissions( "<security><all-permissions/></security>" );
272             jnlpTask.setSignJars( true );
273 
274             jnlpTask.setAlias( keystorealias );
275             jnlpTask.setKeystore( keystore );
276             jnlpTask.setStorePass( keystorepassword );
277             if ( keystoretype != null )
278             {
279                 jnlpTask.setStoreType( keystoretype );
280             }
281             jnlpTask.setProcessJarVersions( processJarVersions );
282 
283             FileSet fs = jnlpTask.createModules();
284             fs.setDir( nbmBuildDirFile );
285             OrSelector or = new OrSelector();
286             AndSelector and = new AndSelector();
287             FilenameSelector inc = new FilenameSelector();
288             inc.setName( "*/modules/**/*.jar" );
289             or.addFilename( inc );
290             inc = new FilenameSelector();
291             inc.setName( "*/lib/**/*.jar" );
292             or.addFilename( inc );
293             inc = new FilenameSelector();
294             inc.setName( "*/core/**/*.jar" );
295             or.addFilename( inc );
296 
297             ModuleSelector ms = new ModuleSelector();
298             Parameter included = new Parameter();
299             included.setName( "includeClusters" );
300             included.setValue( "" );
301             Parameter excluded = new Parameter();
302             excluded.setName( "excludeClusters" );
303             excluded.setValue( "" );
304             Parameter exModules = new Parameter();
305             exModules.setName( "excludeModules" );
306             exModules.setValue( "" );
307             ms.setParameters( new Parameter[]
308                 {
309                     included,
310                     excluded,
311                     exModules
312                 } );
313             and.add( or );
314             and.add( ms );
315             fs.addAnd( and );
316             jnlpTask.execute();
317 
318             //TODO is it really netbeans/
319             String extSnippet = generateExtensions( fs, antProject, "" ); // "netbeans/"
320 
321             if ( masterJnlpFileName == null )
322             {
323                masterJnlpFileName = brandingToken;
324             }
325 
326             Properties props = new Properties();
327             props.setProperty( "jnlp.codebase", localCodebase );
328             props.setProperty( "app.name", brandingToken );
329             props.setProperty( "app.title", project.getName() );
330             if ( project.getOrganization() != null )
331             {
332                 props.setProperty( "app.vendor", project.getOrganization().getName() );
333             }
334             else
335             {
336                 props.setProperty( "app.vendor", "Nobody" );
337             }
338             String description = project.getDescription() != null ? project.getDescription() : "No Project Description";
339             props.setProperty( "app.description", description );
340             props.setProperty( "branding.token", brandingToken );
341             props.setProperty( "master.jnlp.file.name", masterJnlpFileName );
342             props.setProperty( "netbeans.jnlp.fixPolicy", "false" );
343 
344             StringBuilder stBuilder = new StringBuilder();
345             if ( additionalArguments != null )
346             {
347                 StringTokenizer st = new StringTokenizer( additionalArguments );
348                 while ( st.hasMoreTokens() )
349                 {
350                     String arg = st.nextToken();
351                     if ( arg.startsWith( "-J" ) )
352                     {
353                         if ( stBuilder.length() > 0 )
354                         {
355                             stBuilder.append( ' ' );
356                         }
357                         stBuilder.append( arg.substring( 2 ) );
358                     }
359                 }
360             }
361             props.setProperty( "netbeans.run.params", stBuilder.toString() );
362 
363             File masterJnlp = new File(
364                 webstartBuildDir.getAbsolutePath() + File.separator + masterJnlpFileName + ".jnlp" );
365             filterCopy( masterJnlpFile, "master.jnlp", masterJnlp, props );
366 
367 
368             File startup = copyLauncher( outputDirectory, nbmBuildDirFile );
369             File jnlpDestination = new File(
370                 webstartBuildDir.getAbsolutePath() + File.separator + "startup.jar" );
371 
372             SignJar signTask = (SignJar) antProject.createTask( "signjar" );
373             signTask.setKeystore( keystore );
374             signTask.setStorepass( keystorepassword );
375             signTask.setAlias( keystorealias );
376             if ( keystoretype != null )
377             {
378                 signTask.setStoretype( keystoretype );
379             }
380             signTask.setSignedjar( jnlpDestination );
381             signTask.setJar( startup );
382             signTask.execute();
383 
384             //branding
385             DirectoryScanner ds = new DirectoryScanner();
386             ds.setBasedir( nbmBuildDirFile );
387             ds.setIncludes( new String[]
388                 {
389                     "**/locale/*.jar"
390                 } );
391             ds.scan();
392             String[] includes = ds.getIncludedFiles();
393             StringBuilder brandRefs = new StringBuilder();
394             if ( includes != null && includes.length > 0 )
395             {
396                 File brandingDir = new File( webstartBuildDir, "branding" );
397                 brandingDir.mkdirs();
398                 for ( String incBran : includes )
399                 {
400                     File source = new File( nbmBuildDirFile, incBran );
401                     File dest = new File( brandingDir, source.getName() );
402                     FileUtils.copyFile( source, dest );
403                     brandRefs.append( "    <jar href=\'branding/" ).append( dest.getName() ).append( "\'/>\n" );
404                 }
405 
406                 signTask = (SignJar) antProject.createTask( "signjar" );
407                 signTask.setKeystore( keystore );
408                 signTask.setStorepass( keystorepassword );
409                 signTask.setAlias( keystorealias );
410                 if ( keystoretype != null )
411                 {
412                     signTask.setStoretype( keystoretype );
413                 }
414                 
415                 FileSet set = new FileSet();
416                 set.setDir( brandingDir );
417                 set.setIncludes( "*.jar" );
418                 signTask.addFileset( set );
419                 signTask.execute();
420             }
421 
422             File modulesJnlp = new File(
423                 webstartBuildDir.getAbsolutePath() + File.separator + "modules.jnlp" );
424             props.setProperty( "jnlp.branding.jars", brandRefs.toString() );
425             props.setProperty( "jnlp.resources", extSnippet );
426             filterCopy( null, /* filename is historical */"branding.jnlp", modulesJnlp, props );
427 
428             getLog().info( "Verifying generated webstartable content." );
429             VerifyJNLP verifyTask = (VerifyJNLP) antProject.createTask( "verifyjnlp" );
430             FileSet verify = new FileSet();
431             verify.setFile( masterJnlp );
432             verifyTask.addConfiguredFileset( verify );
433             verifyTask.execute();
434 
435 
436             // create zip archive
437             if ( destinationFile.exists() )
438             {
439                 destinationFile.delete();
440             }
441             ZipArchiver archiver = new ZipArchiver();
442             if ( codebase != null )
443             {
444                 getLog().warn( "Defining <codebase>/${nbm.webstart.codebase} is generally unnecessary" );
445                 archiver.addDirectory( webstartBuildDir );
446             }
447             else
448             {
449                 archiver.addDirectory( webstartBuildDir, null, new String[] { "**/*.jnlp" } );
450                 for ( final File jnlp : webstartBuildDir.listFiles() )
451                 {
452                     if ( !jnlp.getName().endsWith( ".jnlp" ) )
453                     {
454                         continue;
455                     }
456                     archiver.addResource( new PlexusIoResource() {
457                         public @Override InputStream getContents() throws IOException
458                         {
459                             return new ByteArrayInputStream( FileUtils.fileRead( jnlp, "UTF-8" ).replace( localCodebase, "$$codebase" ).getBytes( "UTF-8" ) );
460                         }
461                         public @Override long getLastModified()
462                         {
463                             return jnlp.lastModified();
464                         }
465                         public @Override boolean isExisting()
466                         {
467                             return true;
468                         }
469                         public @Override long getSize()
470                         {
471                             return UNKNOWN_RESOURCE_SIZE;
472                         }
473                         public @Override URL getURL() throws IOException
474                         {
475                             return null;
476                         }
477                         public @Override String getName()
478                         {
479                             return jnlp.getAbsolutePath();
480                         }
481                         public @Override boolean isFile()
482                         {
483                             return true;
484                         }
485                         public @Override boolean isDirectory()
486                         {
487                             return false;
488                         }
489                     }, jnlp.getName(), archiver.getDefaultFileMode() );
490                 }
491             }
492             File jdkhome = new File( System.getProperty( "java.home" ) );
493             File servlet = new File( jdkhome, "sample/jnlp/servlet/jnlp-servlet.jar" );
494             if ( ! servlet.isFile() )
495             {
496                 servlet = new File( jdkhome.getParentFile(), "sample/jnlp/servlet/jnlp-servlet.jar" );
497             }
498             if ( servlet.isFile() )
499             {
500                 archiver.addFile( servlet, "WEB-INF/lib/jnlp-servlet.jar" );
501                 archiver.addResource( new PlexusIoResource() {
502                     public @Override InputStream getContents() throws IOException
503                     {
504                         return new ByteArrayInputStream( ( "" +
505                             "<web-app>\n" +
506                             "    <servlet>\n" +
507                             "        <servlet-name>JnlpDownloadServlet</servlet-name>\n" +
508                             "        <servlet-class>jnlp.sample.servlet.JnlpDownloadServlet</servlet-class>\n" +
509                             "    </servlet>\n" +
510                             "    <servlet-mapping>\n" +
511                             "        <servlet-name>JnlpDownloadServlet</servlet-name>\n" +
512                             "        <url-pattern>*.jnlp</url-pattern>\n" +
513                             "    </servlet-mapping>\n" +
514                             "</web-app>\n" ).getBytes() );
515                     }
516                     public @Override long getLastModified()
517                     {
518                         return UNKNOWN_MODIFICATION_DATE;
519                     }
520                     public @Override boolean isExisting()
521                     {
522                         return true;
523                     }
524                     public @Override long getSize()
525                     {
526                         return UNKNOWN_RESOURCE_SIZE;
527                     }
528                     public @Override URL getURL() throws IOException
529                     {
530                         return null;
531                     }
532                     public @Override String getName()
533                     {
534                         return "web.xml";
535                     }
536                     public @Override boolean isFile()
537                     {
538                         return true;
539                     }
540                     public @Override boolean isDirectory()
541                     {
542                         return false;
543                     }
544                 }, "WEB-INF/web.xml", archiver.getDefaultFileMode() );
545             }
546             archiver.setDestFile( destinationFile );
547             archiver.createArchive();
548 
549             // attach standalone so that it gets installed/deployed
550             projectHelper.attachArtifact( project, "war", webstartClassifier, destinationFile );
551 
552         }
553         catch ( Exception ex )
554         {
555             throw new MojoExecutionException( "Error creating webstartable binary.", ex );
556         }
557     }
558 
559     /**
560      * @param standaloneBuildDir
561      * @return The name of the jnlp-launcher jarfile in the build directory
562      */
563     private File copyLauncher( File standaloneBuildDir, File builtInstallation )
564         throws IOException
565     {
566         File jnlpStarter =
567             new File( builtInstallation.getAbsolutePath() + File.separator + "harness" + File.separator + "jnlp"
568                 + File.separator + "jnlp-launcher.jar" );
569         // buffer so it isn't reading a byte at a time!
570         InputStream source = null;
571         FileOutputStream outstream = null;
572         try
573         {
574             if ( !jnlpStarter.exists() )
575             {
576                 source = getClass().getClassLoader().getResourceAsStream(
577                     "harness/jnlp/jnlp-launcher.jar" );
578             }
579             else
580             {
581                 source = new FileInputStream( jnlpStarter );
582             }
583             File jnlpDestination = new File(
584                 standaloneBuildDir.getAbsolutePath() + File.separator + "jnlp-launcher.jar" );
585 
586             outstream = new FileOutputStream( jnlpDestination );
587             IOUtil.copy( source, outstream );
588             return jnlpDestination;
589         }
590         finally
591         {
592             IOUtil.close( source );
593             IOUtil.close( outstream );
594         }
595     }
596 
597     private void filterCopy( File sourceFile, String resourcePath, File destinationFile, Properties filterProperties )
598         throws IOException
599     {
600         // buffer so it isn't reading a byte at a time!
601         Reader source = null;
602         Writer destination = null;
603         try
604         {
605             InputStream instream;
606             if ( sourceFile != null )
607             {
608                 instream = new FileInputStream( sourceFile );
609             }
610             else
611             {
612                 instream = getClass().getClassLoader().getResourceAsStream( resourcePath );
613             }
614             FileOutputStream outstream = new FileOutputStream( destinationFile );
615 
616             source = new BufferedReader( new InputStreamReader( instream, "UTF-8" ) );
617             destination = new OutputStreamWriter( outstream, "UTF-8" );
618 
619             // support ${token}
620             Reader reader = new InterpolationFilterReader( source, filterProperties, "${", "}" );
621 
622             IOUtil.copy( reader, destination );
623         }
624         finally
625         {
626             IOUtil.close( source );
627             IOUtil.close( destination );
628         }
629     }
630 
631     /**
632      * copied from MakeMasterJNLP ant task.
633      * @param files
634      * @param antProject
635      * @param masterPrefix
636      * @return
637      * @throws java.io.IOException
638      */
639     private String generateExtensions( FileSet files, Project antProject, String masterPrefix )
640         throws IOException
641     {
642         StringBuilder buff = new StringBuilder();
643         for ( String nm : files.getDirectoryScanner( antProject ).getIncludedFiles() )
644         {
645             File jar = new File( files.getDir( antProject ), nm );
646 
647             if ( !jar.canRead() )
648             {
649                 throw new IOException( "Cannot read file: " + jar );
650             }
651 
652             JarFile theJar = new JarFile( jar );
653             String codenamebase = theJar.getManifest().getMainAttributes().getValue( "OpenIDE-Module" );
654             if ( codenamebase == null )
655             {
656                 throw new IOException( "Not a NetBeans Module: " + jar );
657             }
658             {
659                 int slash = codenamebase.indexOf( '/' );
660                 if ( slash >= 0 )
661                 {
662                     codenamebase = codenamebase.substring( 0, slash );
663                 }
664             }
665             String dashcnb = codenamebase.replace( '.', '-' );
666 
667             buff.append( "    <extension name='" ).append( codenamebase ).append( "' href='" ).append( masterPrefix ).append( dashcnb ).append( ".jnlp' />\n" );
668             theJar.close();
669         }
670         return buff.toString();
671 
672     }
673 }