1   /*
2    * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3    *
4    * Copyright (c) 2012-2014 Oracle and/or its affiliates. All rights reserved.
5    *
6    * Oracle licenses this file to You under the Apache License, Version 2.0
7    * (the "License"); you may not use this file except in compliance with
8    * the License.  You may obtain a copy of the License at
9    *
10   *      http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   *
18   *
19   * This file incorporates work covered by the following copyright and
20   * permission notice:
21   *
22   * Copyright 2006 Codehaus
23   *
24   * Licensed under the Apache License, Version 2.0 (the "License");
25   * you may not use this file except in compliance with the License.
26   * You may obtain a copy of the License at
27   *
28   *      http://www.apache.org/licenses/LICENSE-2.0
29   *
30   * Unless required by applicable law or agreed to in writing, software
31   * distributed under the License is distributed on an "AS IS" BASIS,
32   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
33   * See the License for the specific language governing permissions and
34   * limitations under the License.
35   */
36  package org.codehaus.mojo.jaxws;
37  
38  import java.io.File;
39  import java.io.FileOutputStream;
40  import java.io.IOException;
41  import java.net.URI;
42  import java.net.URISyntaxException;
43  import java.util.ArrayList;
44  import java.util.Collection;
45  import java.util.HashMap;
46  import java.util.HashSet;
47  import java.util.List;
48  import java.util.Map;
49  import java.util.Properties;
50  import java.util.Set;
51  import java.util.stream.Collectors;
52  
53  import org.apache.maven.artifact.Artifact;
54  import org.apache.maven.artifact.versioning.ArtifactVersion;
55  import org.apache.maven.artifact.versioning.OverConstrainedVersionException;
56  import org.apache.maven.execution.MavenSession;
57  import org.apache.maven.plugin.AbstractMojo;
58  import org.apache.maven.plugin.MojoExecutionException;
59  import org.apache.maven.plugin.MojoFailureException;
60  import org.apache.maven.plugin.descriptor.PluginDescriptor;
61  import org.apache.maven.plugins.annotations.Component;
62  import org.apache.maven.plugins.annotations.Parameter;
63  import org.apache.maven.project.MavenProject;
64  import org.apache.maven.toolchain.Toolchain;
65  import org.apache.maven.toolchain.ToolchainManager;
66  import org.codehaus.plexus.util.Os;
67  import org.codehaus.plexus.util.cli.CommandLineException;
68  import org.codehaus.plexus.util.cli.CommandLineUtils;
69  import org.codehaus.plexus.util.cli.Commandline;
70  import org.codehaus.plexus.util.cli.StreamConsumer;
71  
72  /**
73   *
74   * @author dantran (dantran@apache.org)
75   */
76  abstract class AbstractJaxwsMojo
77      extends AbstractMojo
78  {
79  
80      /**
81       * The Maven Project Object.
82       */
83      @Parameter( defaultValue = "${project}", readonly = true, required = true )
84      protected MavenProject project;
85  
86      /**
87       * Output messages about what the tool is doing.
88       */
89      @Parameter( defaultValue = "false" )
90      protected boolean verbose;
91  
92      /**
93       * Keep generated files.
94       */
95      @Parameter( defaultValue = "true" )
96      protected boolean keep;
97  
98      /**
99       * Allow to use the JAXWS Vendor Extensions.
100      */
101     @Parameter( defaultValue = "false" )
102     private boolean extension;
103 
104     /**
105      * Specify character encoding used by source files.
106      */
107     @Parameter( defaultValue = "${project.build.sourceEncoding}" )
108     protected String encoding;
109 
110     /**
111      * Specify optional command-line options.
112      * <p>
113      * Multiple elements can be specified, and each token must be placed in its own list.
114      * </p>
115      */
116     @Parameter
117     private List<String> args;
118 
119     /**
120      * Specify optional JVM options.
121      * <p>
122      * Multiple elements can be specified, and each token must be placed in its own list.
123      * </p>
124      */
125     @Parameter
126     private List<String> vmArgs;
127 
128     /**
129      * Path to the executable. Should be either <code>wsgen</code> or <code>wsimport</code>
130      * but basically any script which will understand passed in arguments
131      * will work.
132      *
133      * @since 2.2.1
134      */
135     @Parameter
136     private File executable;
137 
138     /**
139      * Information about this plugin, used to lookup this plugin's dependencies
140      * from the currently executing project.
141      *
142      * @since 2.3.1
143      */
144     @Parameter( defaultValue = "${plugin}", readonly = true )
145     protected PluginDescriptor pluginDescriptor;
146 
147     /**
148      * Entry point for toolchains, to get JDK toolchain
149      */
150     @Component
151     private ToolchainManager toolchainManager;
152 
153     /**
154      * If a JDK toolchain is found, by default, it is used to get <code>java</code> executable with its
155      * <code>tools.jar</code>. But if set to <code>true</code>, it is used it to find <code>wsgen</code>
156      * and <code>wsimport</code> executables.
157      *
158      * @since 2.4
159      */
160     @Parameter( defaultValue = "false" )
161     private boolean useJdkToolchainExecutable;
162 
163     /**
164      * The current build session instance. This is used for toolchain manager API calls.
165      */
166     @Parameter( defaultValue = "${session}", readonly = true, required = true )
167     protected MavenSession session;
168 
169     // arguments supported by Metro 2.2/JAXWS RI 2.2.6
170     private static final List<String> METRO_22 = new ArrayList<>();
171 
172     // arguments supported by Metro 2.2.1/JAXWS RI 2.2.7
173     private static final List<String> METRO_221 = new ArrayList<>();
174 
175     // arguments supported by Metro 2.3/JAXWS RI 2.2.8
176     private static final List<String> METRO_23 = new ArrayList<>();
177 
178     static
179     {
180         METRO_22.add( "-encoding" );
181         METRO_22.add( "-clientjar" );
182         METRO_22.add( "-generateJWS" );
183         METRO_22.add( "-implDestDir" );
184         METRO_22.add( "-implServiceName" );
185         METRO_22.add( "-implPortName" );
186 
187         METRO_221.addAll( METRO_22 );
188         METRO_221.add( "-XdisableAuthenticator" );
189 
190         METRO_23.addAll( METRO_221 );
191         METRO_23.add( "-x" );
192     }
193 
194     /**
195      * Main class of the tool to launch when launched as java command.
196      * @return the class name
197      */
198     protected abstract String getMain();
199 
200     /**
201      * Name of the tool to run when launched as JDK executable from JDK Toolchain.
202      * @return the tool name
203      */
204     protected abstract String getToolName();
205 
206     /**
207      * Either <code>${build.outputDirectory}</code> or <code>${build.testOutputDirectory}</code>.
208      * @return the destination directory
209      */
210     protected abstract File getDestDir();
211 
212     protected abstract File getSourceDestDir();
213 
214     protected void addSourceRoot( String sourceDir )
215     {
216         if ( !project.getCompileSourceRoots().contains( sourceDir ) )
217         {
218             getLog().debug( "adding src root: " + sourceDir );
219             project.addCompileSourceRoot( sourceDir );
220         }
221         else
222         {
223             getLog().debug( "existing src root: " + sourceDir );
224         }
225     }
226 
227     protected abstract File getDefaultSrcOut();
228 
229     /**
230      * Checks if compilation after code generation and let generated sources be
231      * compiled by Maven during compilation phase.
232      * @return true if compilation should not be done by the JAX-WS tool
233      */
234     protected abstract boolean isXnocompile();
235 
236     protected String getExtraClasspath()
237     {
238         return null;
239     }
240 
241     protected boolean isExtensionOn()
242     {
243         return extension;
244     }
245 
246     protected List<String> getCommonArgs()
247         throws MojoExecutionException
248     {
249         List<String> commonArgs = new ArrayList<>();
250 
251         if ( !isDefaultSrc( getSourceDestDir() ) || keep )
252         {
253             commonArgs.add( "-keep" );
254             commonArgs.add( "-s" );
255             commonArgs.add( "'" + getSourceDestDir().getAbsolutePath() + "'" );
256             if ( !getSourceDestDir().mkdirs() && !getSourceDestDir().exists() )
257             {
258                 getLog().warn( "Cannot create directory: " + getSourceDestDir().getAbsolutePath() );
259             }
260             addSourceRoot( getSourceDestDir().getAbsolutePath() );
261         }
262 
263         File destDir = getDestDir();
264         if ( !destDir.mkdirs() && !destDir.exists() )
265         {
266             getLog().warn( "Cannot create directory: " + destDir.getAbsolutePath() );
267         }
268         commonArgs.add( "-d" );
269         commonArgs.add( "'" + destDir.getAbsolutePath() + "'" );
270 
271         if ( verbose )
272         {
273             commonArgs.add( "-verbose" );
274         }
275 
276         if ( isArgSupported( "-encoding" ) )
277         {
278             if ( encoding != null )
279             {
280                 commonArgs.add( "-encoding" );
281                 commonArgs.add( encoding );
282             }
283             else
284             {
285                 getLog().warn( "Using platform encoding (" + System.getProperty( "file.encoding" )
286                     + "), build is platform dependent!" );
287             }
288         }
289 
290         if ( isExtensionOn() )
291         {
292             commonArgs.add( "-extension" );
293         }
294 
295         if ( isXnocompile() )
296         {
297             commonArgs.add( "-Xnocompile" );
298         }
299 
300         // add additional command line options
301         if ( args != null )
302         {
303             for ( String arg : args )
304             {
305                 commonArgs.add( arg );
306             }
307         }
308 
309         return commonArgs;
310     }
311 
312     protected boolean isArgSupported( String arg )
313         throws MojoExecutionException
314     {
315         // by default, use latest version supported args
316         List<String> supportedArgs = METRO_23;
317 
318         // then try to find old known versions
319         // try Metro first
320         Artifact a = pluginDescriptor.getArtifactMap().get( "org.glassfish.metro:webservices-tools" );
321         String v = null;
322         if ( a != null )
323         {
324             ArtifactVersion av = getSelectedVersion( a );
325             v = av.toString();
326             if ( av.getMajorVersion() == 2 && av.getMinorVersion() == 2 )
327             {
328                 supportedArgs = av.getIncrementalVersion() == 0 ? METRO_22 : METRO_221;
329             }
330         }
331         else
332         {
333             // fallback to RI
334             a = pluginDescriptor.getArtifactMap().get( "com.sun.xml.ws:jaxws-tools" );
335             ArtifactVersion av = getSelectedVersion( a );
336             v = av.toString();
337             if ( av.getMajorVersion() == 2 && av.getMinorVersion() == 2 )
338             {
339                 if ( av.getIncrementalVersion() == 6 )
340                 {
341                     supportedArgs = METRO_22;
342                 }
343                 else if ( av.getIncrementalVersion() == 7 )
344                 {
345                     supportedArgs = METRO_221;
346                 }
347             }
348         }
349 
350         boolean isSupported = supportedArgs.contains( arg );
351         if ( !isSupported )
352         {
353             getLog().warn( "'" + arg + "' is not supported by " + a.getArtifactId() + ":" + v );
354         }
355         return isSupported;
356     }
357 
358     private static ArtifactVersion getSelectedVersion( Artifact artifact )
359         throws MojoExecutionException
360     {
361         try
362         {
363             return artifact.getSelectedVersion();
364         }
365         catch ( OverConstrainedVersionException ex )
366         {
367             throw new MojoExecutionException( ex.getMessage(), ex );
368         }
369     }
370 
371     private boolean isDefaultSrc( File srcout )
372     {
373         return srcout.equals( getDefaultSrcOut() );
374     }
375 
376     @Override
377     public final void execute()
378         throws MojoExecutionException, MojoFailureException
379     {
380         if ( executable == null && getJdkToolchain() != null && useJdkToolchainExecutable )
381         {
382             // get executable from JDK toolchain
383             executable = new File( getJdkToolchain().findTool( getToolName() ) );
384         }
385 
386         executeJaxws();
387     }
388 
389     public abstract void executeJaxws()
390         throws MojoExecutionException, MojoFailureException;
391 
392     protected void exec( List<String> arguments )
393         throws MojoExecutionException
394     {
395         String launched = "";
396         Commandline cmd = new Commandline();
397 
398         if ( executable != null )
399         {
400             // use JDK wsgen/wsimport or equivalent executable
401             launched = executable.getName();
402             if ( executable.isFile() && executable.canExecute() )
403             {
404                 cmd.setExecutable( executable.getAbsolutePath() );
405                 if ( getExtraClasspath() != null )
406                 {
407                     cmd.createArg().setLine( "-cp" );
408                     cmd.createArg().setValue( getExtraClasspath() );
409                 }
410             }
411             else
412             {
413                 throw new MojoExecutionException( "Cannot execute: " + executable.getAbsolutePath() );
414             }
415         }
416         else
417         {
418             // use tool's class through Invoker as java execution
419             launched = getMain();
420 
421             if ( getJdkToolchain() == null )
422             {
423                 // use java executable from running Maven
424                 cmd.setExecutable( new File( new File( System.getProperty( "java.home" ), "bin" ),
425                                              getJavaExec() ).getAbsolutePath() );
426             }
427             else
428             {
429                 // use java executable from current JDK toolchain
430                 cmd.setExecutable( getJdkToolchain().findTool( "java" ) );
431             }
432 
433             // add additional JVM options
434             if ( vmArgs != null )
435             {
436                 for ( String arg : vmArgs )
437                 {
438                     cmd.createArg().setLine( arg );
439                 }
440             }
441             InvokerCP classpath = getInvokerCP();
442             if ( !isModular() ) {
443                 cmd.createArg().setValue( "-Xbootclasspath/p:" + classpath.ecp );
444             }
445             cmd.createArg().setValue( "-cp" );
446             cmd.createArg().setValue( classpath.invokerPath );
447             cmd.createArg().setLine( Invoker.class.getCanonicalName() );
448             cmd.createArg().setLine( getMain() );
449             String extraCp = getExtraClasspath();
450             String cp = ( ( extraCp != null ) ? ( extraCp + File.pathSeparator ) : "" ) + classpath.cp;
451             try
452             {
453                 File pathFile = createPathFile( cp );
454                 cmd.createArg().setLine( "-pathfile " + "'" + pathFile.getAbsolutePath() +  "'" );
455             }
456             catch ( IOException ioe )
457             {
458                 // creation of temporary file can fail, in such case just put everything on cp
459                 cmd.createArg().setValue( "-cp" );
460                 cmd.createArg().setValue( cp );
461             }
462         }
463 
464         cmd.setWorkingDirectory( project.getBasedir() );
465         for ( String arg : arguments )
466         {
467             cmd.createArg().setLine( arg );
468         }
469 
470         try
471         {
472             String fullCommand = cmd.toString();
473             if ( isWindows() && 8191 <= fullCommand.length() )
474             {
475                 getLog().warn( "Length of Windows command line is limited to 8191 characters, but current command has "
476                     + fullCommand.length() + " characters:" );
477                 getLog().warn( fullCommand );
478             }
479             else
480             {
481                 getLog().debug( fullCommand );
482             }
483 
484             // The DefaultConsumer class in plexus-utils-3.1.0 first calls
485             // System.out.println() to print the message and then throws an
486             // IOException when System.out.checkError() returns true. This
487             // results in a MojoExecutionException when the plugin is being used
488             // in Eclipse.
489             // In contrary to this the implementation in plexus-utils-3.0.2x
490             // simply calls System.out.println() without further checks...
491             StreamConsumer sc = new StreamConsumer() {
492                 public void consumeLine( String line )
493                 {
494                     System.out.println( line );
495                 }
496             };
497             // StreamConsumer sc = new DefaultConsumer();
498             if ( CommandLineUtils.executeCommandLine( cmd, sc, sc ) != 0 )
499             {
500                 throw new MojoExecutionException( "Invocation of " + launched + " failed - check output" );
501             }
502         }
503         catch ( CommandLineException t )
504         {
505             throw new MojoExecutionException( t.getMessage(), t );
506         }
507     }
508 
509     private boolean isModular() {
510         try
511         {
512             Class<?> moduleClass = Class.forName( "java.lang.Module" );
513             return moduleClass != null;
514         }
515         catch ( ClassNotFoundException e )
516         {
517             return false;
518         }
519     }
520 
521     protected void maybeUnsupportedOption( String option, String value, List<String> arguments )
522     {
523         if ( executable == null )
524         {
525             arguments.add( option );
526             if ( value != null )
527             {
528                 arguments.add( value );
529             }
530         }
531         else
532         {
533             getLog().warn( option + " may not supported on older JDKs.\n"
534                 + "Use <args> to bypass this warning if you really want to use it." );
535         }
536     }
537 
538     /**
539      * Calculates 3 classpaths used to launch tools as class through Invoker.
540      * @return Invoker's classpath
541      * @see Invoker
542      */
543     private InvokerCP getInvokerCP()
544     {
545         Set<Artifact> endorsedArtifacts = new HashSet<>();
546         Map<String, Artifact> artifactsMap = new HashMap<>();
547         for ( Artifact a : pluginDescriptor.getArtifacts() )
548         {
549             addArtifactToCp( a, artifactsMap, endorsedArtifacts );
550         }
551 
552         StringBuilder cp = new StringBuilder( getCPasString( artifactsMap.values() ) );
553         StringBuilder ecp = new StringBuilder( getCPasString( endorsedArtifacts ) );
554 
555         String invokerPath = null;
556         try
557         {
558             invokerPath = Invoker.class.getProtectionDomain().getCodeSource().getLocation().toExternalForm();
559             invokerPath = new URI( invokerPath.substring( 5 ) ).getPath();
560         }
561         catch ( URISyntaxException ex )
562         {
563             throw new RuntimeException( ex );
564         }
565 
566         // add custom invoker path to normal classpath
567         if ( cp.length() > 0 ) {
568             cp.append( File.pathSeparator );
569         }
570         cp.append( invokerPath );
571 
572         // don't forget tools.jar
573         String javaHome = getJavaHome();
574         File toolsJar = new File( javaHome, "../lib/tools.jar" );
575         if ( !toolsJar.exists() )
576         {
577             toolsJar = new File( javaHome, "lib/tools.jar" );
578         }
579         // Java >= 9 doesn't have a tools.jar anymore
580         if ( toolsJar.exists() ) {
581             if ( cp.length() > 0 ) {
582                 cp.append( File.pathSeparator );
583             }
584             cp.append( toolsJar.getAbsolutePath() );
585         }
586 
587         if ( getLog().isDebugEnabled() )
588         {
589             getLog().debug( "getInvokerCP():\n"
590                             + "    endorsed: " + toString( endorsedArtifacts ) + "\n"
591                             + "    classpath: " + toString( artifactsMap.values() ) + "\n"
592                             + "    ecp: " + ecp + "\n"
593                             + "    cp: " + cp + "\n"
594                             + "    invokerPath: " + invokerPath );
595         }
596 
597         return new InvokerCP( ecp.toString(), cp.toString(), invokerPath );
598     }
599 
600     private static class InvokerCP
601     {
602         public final String ecp;
603 
604         public final String cp;
605 
606         public final String invokerPath;
607 
608         public InvokerCP( String ecp, String cp, String invokerPath )
609         {
610             this.ecp = ecp;
611             this.cp = cp;
612             this.invokerPath = invokerPath;
613         }
614     }
615 
616     private String getJavaExec()
617     {
618         return isWindows() ? "java.exe" : "java";
619     }
620 
621     private String getJavaHome()
622     {
623         // by default, java.home from JDK/JRE running Maven
624         String javaHome = System.getProperty( "java.home" );
625         if ( getJdkToolchain() != null )
626         {
627             // JDK toolchain used
628             File javaExecutable = new File( getJdkToolchain().findTool( "java" ) ); // ${java.home}/bin/java
629             javaHome = javaExecutable.getParentFile().getParent();
630         }
631         return javaHome;
632     }
633 
634     private File createPathFile( String cp )
635         throws IOException
636     {
637         File f = File.createTempFile( "jax-ws-mvn-plugin-cp", ".txt" );
638         if ( f.exists() && f.isFile() && !f.delete() )
639         {
640             // this should not happen
641             getLog().warn( "cannot remove obsolete classpath setting file: " + f.getAbsolutePath() );
642         }
643         Properties p = new Properties();
644         p.put( "cp", cp.replace( File.separatorChar, '/' ) );
645         getLog().debug( "stored classpath: " + cp.replace( File.separatorChar, '/' ) );
646         try (FileOutputStream fos = new FileOutputStream( f )) {
647             p.store( fos, null );
648         }
649         catch ( IOException ex )
650         {
651             getLog().error( ex );
652         }
653         return f;
654     }
655 
656     private boolean isWindows()
657     {
658         return Os.isFamily( Os.FAMILY_WINDOWS );
659     }
660 
661     protected String getCPasString( Collection<Artifact> artifacts )
662     {
663         return artifacts.stream().map( a -> a.getFile().getAbsolutePath() ).collect( Collectors.joining( File.pathSeparator ) );
664     }
665 
666     private String toString(Collection<Artifact> artifacts) {
667         return artifacts
668                 .stream().map( a -> String.join( ":", a.getGroupId(), a.getArtifactId(), a.getVersion() ) )
669                 .collect( Collectors.joining( " " ) );
670     }
671 
672     /**
673      * Places the artifact in either the endorsed artifacts set or the normal
674      * artifacts map.  It will only add those in "compile" and "runtime" scope
675      * or those that are specifically endorsed.
676      * 
677      * @param a artifact to sort
678      * @param artifactsMap normal artifacts map
679      * @param endorsedArtifacts endorsed artifacts set
680      */
681     private void addArtifactToCp( Artifact a, Map<String, Artifact> artifactsMap, Set<Artifact> endorsedArtifacts )
682     {
683         if ( !isModular() && isEndorsedArtifact( a ) )
684         {
685             endorsedArtifacts.add( a );
686         }
687         else if ( "compile".equals( a.getScope() ) || "runtime".equals( a.getScope() ) )
688         {
689             artifactsMap.put( a.getGroupId() + ":" + a.getArtifactId(), a );
690         }
691     }
692 
693     protected void addVmArg( String vmArg )
694     {
695         if ( vmArgs == null )
696         {
697             vmArgs = new ArrayList<>();
698         }
699         vmArgs.add( vmArg );
700     }
701 
702     private boolean isEndorsedArtifact( Artifact a )
703     {
704         return "jaxws-api".equals( a.getArtifactId() )
705                 || "jaxb-api".equals( a.getArtifactId() )
706                 || "saaj-api".equals( a.getArtifactId() )
707                 || "jsr181-api".equals( a.getArtifactId() )
708                 || "javax.annotation".equals( a.getArtifactId() )
709                 || "javax.annotation-api".equals( a.getArtifactId() )
710                 || "webservices-api".equals( a.getArtifactId() )
711                 || a.getArtifactId().startsWith( "javax.xml.ws" )
712                 || a.getArtifactId().startsWith( "javax.xml.bind" );
713     }
714 
715     private Toolchain getJdkToolchain()
716     {
717         Toolchain tc = null;
718         if ( toolchainManager != null )
719         {
720             tc = toolchainManager.getToolchainFromBuildContext( "jdk", session );
721         }
722         return tc;
723     }
724 }