package org.codehaus.mojo.webstart; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.metadata.ArtifactMetadataSource; import org.apache.maven.artifact.resolver.ArtifactNotFoundException; import org.apache.maven.artifact.resolver.ArtifactResolutionException; import org.apache.maven.artifact.resolver.ArtifactResolutionResult; import org.apache.maven.artifact.resolver.filter.ScopeArtifactFilter; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.project.MavenProject; import org.codehaus.mojo.webstart.generator.GeneratorExtraConfig; import org.codehaus.mojo.webstart.generator.JarResourcesGenerator; import org.codehaus.mojo.webstart.generator.VersionXmlGenerator; import org.codehaus.plexus.util.FileUtils; import org.codehaus.plexus.util.StringUtils; /** * This MOJO is tailored for use within a Maven web application project that uses * the JnlpDownloadServlet to serve up the JNLP application. * * @author Kevin Stembridge * @since 1.0-alpha-2 * @version $Id$ * @goal jnlp-download-servlet * @requiresDependencyResolution runtime * @requiresProject * @inheritedByDefault true */ public class JnlpDownloadServletMojo extends AbstractBaseJnlpMojo { /** * @parameter default-value="${project}" * @required * @readonly */ private MavenProject project; /** * The project's artifact metadata source, used to resolve transitive dependencies. * @component * @required * @readonly */ private ArtifactMetadataSource artifactMetadataSource; /** * The name of the directory into which the jnlp file and other * artifacts will be stored after processing. This directory will be created * directly within the root of the WAR produced by the enclosing project. * * @parameter default-value="webstart" */ private String outputDirectoryName; /** * The collection of JnlpFile configuration elements. Each one represents a * JNLP file that is to be generated and deployed within the enclosing * project's WAR artifact. At least one JnlpFile must be specified. * * @parameter * @required */ private List/*JnlpFile*/ jnlpFiles; /** * The configurable collection of jars that are common to all jnlpFile elements declared in * plugin configuration. These jars will be output as jar elements in the resources section of * every generated JNLP file and bundled into the specified output directory of the artifact * produced by the project. * * @parameter */ private List/*JarResource*/ commonJarResources; /** * Creates a new uninitialized {@code JnlpDownloadServletMojo}. */ public JnlpDownloadServletMojo() { // do nothing } /** * {@inheritDoc} */ public void execute() throws MojoExecutionException, MojoFailureException { checkConfiguration(); try { copyResources( getResourcesDirectory(), getWorkDirectory() ); } catch ( IOException e ) { throw new MojoExecutionException( "An error occurred attempting to copy " + "resources to the working directory.", e ); } if ( this.commonJarResources != null ) { retrieveJarResources( this.commonJarResources ); } for ( Iterator itr = this.jnlpFiles.iterator(); itr.hasNext(); ) { JnlpFile jnlpFile = ( JnlpFile ) itr.next(); retrieveJarResources( jnlpFile.getJarResources() ); } signOrRenameJars(); packJars(); for ( Iterator itr = this.jnlpFiles.iterator(); itr.hasNext(); ) { generateJnlpFile( ( JnlpFile ) itr.next(), getLibPath() ); } generateVersionXml(); copyWorkingDirToOutputDir(); } /** * Confirms that all plugin configuration provided by the user * in the pom.xml file is valid. * * @throws MojoExecutionException if any user configuration is invalid. */ private void checkConfiguration() throws MojoExecutionException { if ( this.jnlpFiles.isEmpty() ) { throw new MojoExecutionException( "Configuration error: At least one <jnlpFile> element must be specified" ); } if ( this.jnlpFiles.size() == 1 && StringUtils.isEmpty( ( ( JnlpFile ) this.jnlpFiles.get( 0 ) ).getOutputFilename() ) ) { getLog().debug( "Jnlp output file name not specified in single set of jnlpFiles. " + "Using default output file name: launch.jnlp." ); ( ( JnlpFile ) this.jnlpFiles.get( 0 ) ).setOutputFilename( "launch.jnlp" ); } for ( Iterator itr = this.jnlpFiles.iterator(); itr.hasNext(); ) { checkJnlpFileConfiguration( ( JnlpFile ) itr.next() ); } checkForDuplicateJarResources(); checkCommonJarResources(); checkForUniqueJnlpFilenames(); checkPack200(); } /** * Checks the validity of a single jnlpFile configuration element. * * @param jnlpFile The configuration element to be checked. * @throws MojoExecutionException if the config element is invalid. */ private void checkJnlpFileConfiguration( JnlpFile jnlpFile ) throws MojoExecutionException { if ( StringUtils.isEmpty( jnlpFile.getOutputFilename() ) ) { throw new MojoExecutionException( "Configuration error: An outputFilename must be specified for each jnlpFile element" ); } if ( jnlpFile.getTemplateFilename() == null ) { getLog().info( "No templateFilename found for " + jnlpFile.getOutputFilename() + ". Will use the default template." ); } else { File templateFile = new File( getTemplateDirectory(), jnlpFile.getTemplateFilename() ); if ( !templateFile.isFile() ) { throw new MojoExecutionException( "The specified JNLP template does not exist: [" + templateFile + "]" ); } } checkJnlpJarResources( jnlpFile ); } /** * Checks the collection of jarResources configured for a given jnlpFile element. * * @param jnlpFile The configuration element whose jarResources are to be checked. * @throws MojoExecutionException if any config is invalid. */ private void checkJnlpJarResources( JnlpFile jnlpFile ) throws MojoExecutionException { List jnlpJarResources = jnlpFile.getJarResources(); if ( jnlpJarResources == null || jnlpJarResources.isEmpty() ) { throw new MojoExecutionException( "Configuration error: A non-empty <jarResources> element must be specified in the plugin " + "configuration for the JNLP file named [" + jnlpFile.getOutputFilename() + "]" ); } Iterator itr = jnlpJarResources.iterator(); List/*JarResource*/ jarsWithMainClass = new ArrayList(); while ( itr.hasNext() ) { JarResource jarResource = (JarResource) itr.next(); checkMandatoryJarResourceFields( jarResource ); if ( jarResource.getMainClass() != null ) { jnlpFile.setMainClass( jarResource.getMainClass() ); jarsWithMainClass.add( jarResource ); } } if ( jarsWithMainClass.isEmpty() ) { throw new MojoExecutionException( "Configuration error: Exactly one <jarResource> element must " + "be declared with a <mainClass> element in the configuration for JNLP file [" + jnlpFile.getOutputFilename() + "]" ); } if ( jarsWithMainClass.size() > 1 ) { throw new MojoExecutionException( "Configuration error: More than one <jarResource> element has been declared " + "with a <mainClass> element in the configuration for JNLP file [" + jnlpFile.getOutputFilename() + "]" ); } } /** * Checks that any jarResources defined in the jnlpFile elements are not also defined in * commonJarResources. * @throws MojoExecutionException if a duplicate is found. */ private void checkForDuplicateJarResources() throws MojoExecutionException { if ( this.commonJarResources == null || this.commonJarResources.isEmpty() ) { return; } for ( Iterator jnlpFileItr = this.jnlpFiles.iterator(); jnlpFileItr.hasNext(); ) { JnlpFile jnlpFile = (JnlpFile) jnlpFileItr.next(); List jnlpJarResources = jnlpFile.getJarResources(); for ( Iterator jarResourceItr = jnlpJarResources.iterator(); jarResourceItr.hasNext(); ) { JarResource jarResource = (JarResource) jarResourceItr.next(); if ( this.commonJarResources.contains( jarResource ) ) { String message = "Configuration Error: The jar resource element for artifact " + jarResource + " defined in common jar resources is duplicated in the jar " + "resources configuration of the jnlp file identified by the template file " + jnlpFile.getTemplateFilename() + "."; throw new MojoExecutionException( message ); } } } } /** * Checks the configuration of common jar resources. Specifying common jar * resources is optional but if present, each jar resource must have the * same mandatory fields as jar resources configured directly within a * jnlpFile element, but it must not have a configured mainClass element. * * @throws MojoExecutionException if the config is invalid. */ private void checkCommonJarResources( ) throws MojoExecutionException { if ( this.commonJarResources == null ) { return; } for ( Iterator itr = this.commonJarResources.iterator(); itr.hasNext(); ) { JarResource jarResource = (JarResource) itr.next(); checkMandatoryJarResourceFields( jarResource ); if ( jarResource.getMainClass() != null ) { throw new MojoExecutionException( "Configuration Error: A mainClass must not be specified " + "on a JarResource in the commonJarResources collection." ); } } } private void checkMandatoryJarResourceFields( JarResource jarResource ) throws MojoExecutionException { if ( StringUtils.isEmpty( jarResource.getGroupId() ) || StringUtils.isEmpty( jarResource.getArtifactId() ) || StringUtils.isEmpty( jarResource.getVersion() ) ) { throw new MojoExecutionException( "Configuration error: groupId, artifactId or version missing for jarResource[" + jarResource + "]." ); } } /** * Confirms that each jnlpFile element is configured with a unique JNLP file name. * * @throws MojoExecutionException */ private void checkForUniqueJnlpFilenames() throws MojoExecutionException { Set filenames = new HashSet( this.jnlpFiles.size() ); for ( Iterator itr = this.jnlpFiles.iterator(); itr.hasNext(); ) { JnlpFile jnlpFile = (JnlpFile) itr.next(); if ( !filenames.add( jnlpFile.getOutputFilename() ) ) { throw new MojoExecutionException( "Configuration error: Unique JNLP filenames must be provided. " + "The following file name appears more than once [" + jnlpFile.getOutputFilename() + "]." ); } } } /** * Resolve the artifacts represented by the given collection of JarResources and * copy them to the working directory if a newer copy of the file doesn't already * exist there. Transitive dependencies will also be retrieved. * <p> * Transitive dependencies are added to the list specified as parameter. TODO fix that. * * @throws MojoExecutionException */ private void retrieveJarResources( List jarResources ) throws MojoExecutionException { Set jarResourceArtifacts = new HashSet(); try { //for each configured JarResource, create and resolve the corresponding artifact and //check it for the mainClass if specified for ( Iterator itr = jarResources.iterator(); itr.hasNext(); ) { JarResource jarResource = ( JarResource ) itr.next(); Artifact artifact = createArtifact( jarResource ); getArtifactResolver().resolve( artifact, getRemoteRepositories(), getLocalRepository() ); jarResource.setArtifact( artifact ); checkForMainClass( jarResource ); jarResourceArtifacts.add( artifact ); } if ( !isExcludeTransitive() ) { retrieveTransitiveDependencies( jarResourceArtifacts , jarResources ); } //for each JarResource, copy its artifact to the lib directory if necessary for ( Iterator itr = jarResources.iterator(); itr.hasNext(); ) { JarResource jarResource = ( JarResource ) itr.next(); Artifact artifact = jarResource.getArtifact(); boolean copied = copyJarAsUnprocessedToDirectoryIfNecessary( artifact.getFile(), getLibDirectory() ); if ( copied ) { String name = artifact.getFile().getName(); if ( getLog().isDebugEnabled() ) { getLog().debug( "Adding " + name + " to modifiedJnlpArtifacts list." ); } getModifiedJnlpArtifacts().add( name.substring( 0, name.lastIndexOf( '.' ) ) ); } if ( jarResource.isOutputJarVersion() ) { // Create and set a version-less href for this jarResource String hrefValue = buildHrefValue( artifact ); jarResource.setHrefValue( hrefValue ); } } } catch ( ArtifactResolutionException e ) { throw new MojoExecutionException( "Unable to resolve an artifact", e ); } catch ( ArtifactNotFoundException e ) { throw new MojoExecutionException( "Unable to find an artifact", e ); } catch ( IOException e ) { throw new MojoExecutionException( "Unable to copy an artifact to the working directory", e ); } } private Artifact createArtifact( JarResource jarResource ) { if ( jarResource.getClassifier() == null ) { return getArtifactFactory().createArtifact( jarResource.getGroupId(), jarResource.getArtifactId(), jarResource.getVersion(), Artifact.SCOPE_RUNTIME, "jar" ); } else { return getArtifactFactory().createArtifactWithClassifier( jarResource.getGroupId(), jarResource.getArtifactId(), jarResource.getVersion(), "jar", jarResource.getClassifier() ); } } /** * If the given jarResource is configured with a main class, the underlying artifact * is checked to see if it actually contains the specified class. * * @param jarResource * @throws IllegalStateException if the jarResource's underlying artifact has not yet been resolved. * @throws MojoExecutionException */ private void checkForMainClass( JarResource jarResource ) throws MojoExecutionException { String mainClass = jarResource.getMainClass(); if ( mainClass == null ) { return; } Artifact artifact = jarResource.getArtifact(); if ( artifact == null ) { throw new IllegalStateException( "Implementation Error: The given jarResource cannot be checked for " + "a main class until the underlying artifact has been resolved: [" + jarResource + "]" ); } try { if ( !artifactContainsClass( artifact, mainClass ) ) { throw new MojoExecutionException( "The jar specified by the following jarResource does not contain the declared main class:" + jarResource ); } } catch ( MalformedURLException e ) { throw new MojoExecutionException( "Attempting to find main class [" + mainClass + "] in [" + artifact + "]", e ); } } private void retrieveTransitiveDependencies( Set jarResourceArtifacts , List jarResources ) throws ArtifactResolutionException, ArtifactNotFoundException { // this restricts to runtime and compile scope ScopeArtifactFilter artifactFilter = new ScopeArtifactFilter( Artifact.SCOPE_RUNTIME ); ArtifactResolutionResult result = getArtifactResolver().resolveTransitively( jarResourceArtifacts, getProject().getArtifact(), null, //managedVersions getLocalRepository(), getRemoteRepositories(), this.artifactMetadataSource, artifactFilter ); Set transitiveResolvedArtifacts = result.getArtifacts(); if ( getLog().isDebugEnabled() ) { getLog().debug( "transitively resolved artifacts = " + transitiveResolvedArtifacts ); getLog().debug( "jarResources = " + jarResources ); getLog().debug( "jarResourceArtifacts = " + jarResourceArtifacts ); } //for each transitive dependency, wrap it in a JarResource and add it to the collection of //existing jar resources for ( Iterator itr = transitiveResolvedArtifacts.iterator(); itr.hasNext(); ) { Artifact resolvedArtifact = (Artifact) itr.next(); // this whole double check is ugly as well as this method changing the input variable // we should really improve the way we collect the jarResources if ( !jarResourceArtifacts.contains( resolvedArtifact ) ) { JarResource newJarResource = new JarResource( resolvedArtifact ); if ( !jarResources.contains( newJarResource ) ) { newJarResource.setOutputJarVersion( true ); jarResources.add( newJarResource ); } } } } private void generateJnlpFile( JnlpFile jnlpFile, String libPath ) throws MojoExecutionException { File jnlpOutputFile = new File( getWorkDirectory(), jnlpFile.getOutputFilename() ); Set jarResources = new LinkedHashSet(); jarResources.addAll( jnlpFile.getJarResources() ); if ( this.commonJarResources != null && !this.commonJarResources.isEmpty() ) { for ( Iterator itr = this.commonJarResources.iterator(); itr.hasNext(); ) { JarResource jarResource = ( JarResource ) itr.next(); jarResources.add( jarResource ); } jarResources.addAll( this.commonJarResources ); } JarResourcesGenerator jnlpGenerator = new JarResourcesGenerator( getProject(), getTemplateDirectory(), "default-jnlp-servlet-template.vm", jnlpOutputFile, jnlpFile.getTemplateFilename(), jarResources, jnlpFile.getMainClass(), getWebstartJarURLForVelocity(), libPath ); jnlpGenerator.setExtraConfig( getGeneratorExtraConfig() ); try { jnlpGenerator.generate(); } catch ( Exception e ) { throw new MojoExecutionException( "The following error occurred attempting to generate " + "the JNLP deployment descriptor: " + e, e ); } } private GeneratorExtraConfig getGeneratorExtraConfig() { return new GeneratorExtraConfig() { public String getJnlpSpec() { return "1.0+"; } public String getOfflineAllowed() { return "false"; } public String getAllPermissions() { return "true"; } public String getJ2seVersion() { return "1.5+"; } }; } /** * Generates a version.xml file for all the jarResources configured either in jnlpFile elements * or in the commonJarResources element. * * @throws MojoExecutionException */ private void generateVersionXml() throws MojoExecutionException { Set/*JarResource*/ jarResources = new LinkedHashSet(); //combine the jar resources from commonJarResources and each JnlpFile config for ( Iterator itr = this.jnlpFiles.iterator(); itr.hasNext(); ) { JnlpFile jnlpFile = ( JnlpFile ) itr.next(); jarResources.addAll( jnlpFile.getJarResources() ); } if ( this.commonJarResources != null ) { jarResources.addAll( this.commonJarResources ); } VersionXmlGenerator generator = new VersionXmlGenerator(); generator.generate( getLibDirectory(), jarResources ); } /** * {@inheritDoc} */ public MavenProject getProject() { return this.project; } /** * Builds the string to be entered in the href attribute of the jar * resource element in the generated JNLP file. This will be equal * to the artifact file name with the version number stripped out. * * @param artifact The underlying artifact of the jar resource. * @return The href string for the given artifact, never null. */ private String buildHrefValue( Artifact artifact ) { StringBuffer sbuf = new StringBuffer(); sbuf.append( artifact.getArtifactId() ); if ( StringUtils.isNotEmpty( artifact.getClassifier() ) ) { sbuf.append( "-" ).append( artifact.getClassifier() ); } sbuf.append( "." ).append( artifact.getArtifactHandler().getExtension() ); return sbuf.toString(); } /** * Copies the contents of the working directory to the output directory. */ private void copyWorkingDirToOutputDir() throws MojoExecutionException { File outputDir = new File( getProject().getBuild().getDirectory(), getProject().getBuild().getFinalName() + File.separator + this.outputDirectoryName ); if ( !outputDir.exists() ) { if ( getLog().isInfoEnabled() ) { getLog().info( "Creating JNLP output directory: " + outputDir.getAbsolutePath() ); } if ( !outputDir.mkdirs() ) { throw new MojoExecutionException( "Unable to create the output directory for the jnlp bundle" ); } } try { FileUtils.copyDirectoryStructure( getWorkDirectory(), outputDir ); } catch ( IOException e ) { throw new MojoExecutionException( "An error occurred attempting to copy a file to the JNLP output directory.", e ); } } }