View Javadoc
1   package org.apache.maven.plugin.surefire.booterclient;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *     http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.OutputStreamFlushableCommandline;
23  import org.apache.maven.plugin.surefire.booterclient.output.InPluginProcessDumpSingleton;
24  import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
25  import org.apache.maven.surefire.booter.Classpath;
26  import org.apache.maven.surefire.booter.StartupConfiguration;
27  import org.apache.maven.surefire.booter.SurefireBooterForkException;
28  
29  import javax.annotation.Nonnull;
30  import javax.annotation.Nullable;
31  import java.io.File;
32  import java.io.FileOutputStream;
33  import java.io.IOException;
34  import java.net.URI;
35  import java.net.URISyntaxException;
36  import java.nio.file.Path;
37  import java.nio.file.Paths;
38  import java.util.Iterator;
39  import java.util.List;
40  import java.util.Map;
41  import java.util.Properties;
42  import java.util.jar.JarEntry;
43  import java.util.jar.JarOutputStream;
44  import java.util.jar.Manifest;
45  
46  import static java.nio.file.Files.isDirectory;
47  import static org.apache.maven.plugin.surefire.SurefireHelper.escapeToPlatformPath;
48  import static org.apache.maven.surefire.util.internal.StringUtils.NL;
49  
50  /**
51   * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
52   * @since 2.21.0.Jigsaw
53   */
54  public final class JarManifestForkConfiguration
55      extends AbstractClasspathForkConfiguration
56  {
57      @SuppressWarnings( "checkstyle:parameternumber" )
58      public JarManifestForkConfiguration( @Nonnull Classpath bootClasspath, @Nonnull File tempDirectory,
59                                           @Nullable String debugLine, @Nonnull File workingDirectory,
60                                           @Nonnull Properties modelProperties, @Nullable String argLine,
61                                           @Nonnull Map<String, String> environmentVariables, boolean debug,
62                                           int forkCount, boolean reuseForks, @Nonnull Platform pluginPlatform,
63                                           @Nonnull ConsoleLogger log )
64      {
65          super( bootClasspath, tempDirectory, debugLine, workingDirectory, modelProperties, argLine,
66                  environmentVariables, debug, forkCount, reuseForks, pluginPlatform, log );
67      }
68  
69      @Override
70      protected void resolveClasspath( @Nonnull OutputStreamFlushableCommandline cli,
71                                       @Nonnull String booterThatHasMainMethod,
72                                       @Nonnull StartupConfiguration config,
73                                       @Nonnull File dumpLogDirectory )
74              throws SurefireBooterForkException
75      {
76          try
77          {
78              File jar = createJar( toCompleteClasspath( config ), booterThatHasMainMethod, dumpLogDirectory );
79              cli.createArg().setValue( "-jar" );
80              cli.createArg().setValue( escapeToPlatformPath( jar.getAbsolutePath() ) );
81          }
82          catch ( IOException e )
83          {
84              throw new SurefireBooterForkException( "Error creating archive file", e );
85          }
86      }
87  
88      /**
89       * Create a jar with just a manifest containing a Main-Class entry for BooterConfiguration and a Class-Path entry
90       * for all classpath elements.
91       *
92       * @param classPath      List&lt;String&gt; of all classpath elements.
93       * @param startClassName The class name to start (main-class)
94       * @return file of the jar
95       * @throws IOException When a file operation fails.
96       */
97      @Nonnull
98      private File createJar( @Nonnull List<String> classPath, @Nonnull String startClassName,
99                              @Nonnull File dumpLogDirectory )
100             throws IOException
101     {
102         File file = File.createTempFile( "surefirebooter", ".jar", getTempDirectory() );
103         if ( !isDebug() )
104         {
105             file.deleteOnExit();
106         }
107         Path parent = file.getParentFile().toPath();
108         FileOutputStream fos = new FileOutputStream( file );
109         try ( JarOutputStream jos = new JarOutputStream( fos ) )
110         {
111             jos.setLevel( JarOutputStream.STORED );
112             JarEntry je = new JarEntry( "META-INF/MANIFEST.MF" );
113             jos.putNextEntry( je );
114 
115             Manifest man = new Manifest();
116 
117             boolean dumpError = true;
118 
119             // we can't use StringUtils.join here since we need to add a '/' to
120             // the end of directory entries - otherwise the jvm will ignore them.
121             StringBuilder cp = new StringBuilder();
122             for ( Iterator<String> it = classPath.iterator(); it.hasNext(); )
123             {
124                 Path classPathElement = Paths.get( it.next() );
125                 ClasspathElementUri classpathElementUri =
126                         toClasspathElementUri( parent, classPathElement, dumpLogDirectory, dumpError );
127                 // too many errors in dump file with the same root cause may slow down the Boot Manifest-JAR startup
128                 dumpError &= !classpathElementUri.absolute;
129                 cp.append( classpathElementUri.uri );
130                 if ( isDirectory( classPathElement ) && !classpathElementUri.uri.endsWith( "/" ) )
131                 {
132                     cp.append( '/' );
133                 }
134 
135                 if ( it.hasNext() )
136                 {
137                     cp.append( ' ' );
138                 }
139             }
140 
141             man.getMainAttributes().putValue( "Manifest-Version", "1.0" );
142             man.getMainAttributes().putValue( "Class-Path", cp.toString().trim() );
143             man.getMainAttributes().putValue( "Main-Class", startClassName );
144 
145             man.write( jos );
146 
147             jos.closeEntry();
148             jos.flush();
149 
150             return file;
151         }
152     }
153 
154     static String relativize( @Nonnull Path parent, @Nonnull Path child )
155             throws IllegalArgumentException
156     {
157         return parent.relativize( child )
158                 .toString();
159     }
160 
161     static String toAbsoluteUri( @Nonnull Path absolutePath )
162     {
163         return absolutePath.toUri()
164                 .toASCIIString();
165     }
166 
167     static ClasspathElementUri toClasspathElementUri( @Nonnull Path parent,
168                                          @Nonnull Path classPathElement,
169                                          @Nonnull File dumpLogDirectory,
170                                          boolean dumpError )
171             throws IOException
172     {
173         try
174         {
175             String relativeUriPath = relativize( parent, classPathElement )
176                     .replace( '\\', '/' );
177 
178             return new ClasspathElementUri( new URI( null, relativeUriPath, null ) );
179         }
180         catch ( IllegalArgumentException e )
181         {
182             if ( dumpError )
183             {
184                 String error = "Boot Manifest-JAR contains absolute paths in classpath '"
185                         + classPathElement
186                         + "'"
187                         + NL
188                         + "Hint: <argLine>-Djdk.net.URLClassPath.disableClassPathURLCheck=true</argLine>";
189                 InPluginProcessDumpSingleton.getSingleton()
190                         .dumpStreamText( error, dumpLogDirectory );
191             }
192 
193             return new ClasspathElementUri( toAbsoluteUri( classPathElement ) );
194         }
195         catch ( URISyntaxException e )
196         {
197             // This is really unexpected, so fail
198             throw new IOException( "Could not create a relative path "
199                     + classPathElement
200                     + " against "
201                     + parent, e );
202         }
203     }
204 
205     static final class ClasspathElementUri
206     {
207         final String uri;
208         final boolean absolute;
209 
210         ClasspathElementUri( String uri )
211         {
212             this.uri = uri;
213             absolute = true;
214         }
215 
216         ClasspathElementUri( URI uri )
217         {
218             this.uri = uri.toASCIIString();
219             absolute = false;
220         }
221     }
222 }