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.commons.compress.archivers.zip.Zip64Mode;
23  import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
24  import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
25  import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.OutputStreamFlushableCommandline;
26  import org.apache.maven.plugin.surefire.booterclient.output.InPluginProcessDumpSingleton;
27  import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
28  import org.apache.maven.surefire.booter.Classpath;
29  import org.apache.maven.surefire.booter.StartupConfiguration;
30  import org.apache.maven.surefire.booter.SurefireBooterForkException;
31  
32  import javax.annotation.Nonnull;
33  import javax.annotation.Nullable;
34  import java.io.BufferedOutputStream;
35  import java.io.File;
36  import java.io.FileOutputStream;
37  import java.io.IOException;
38  import java.io.OutputStream;
39  import java.io.UnsupportedEncodingException;
40  import java.net.URLEncoder;
41  import java.nio.charset.Charset;
42  import java.nio.file.Path;
43  import java.nio.file.Paths;
44  import java.util.Iterator;
45  import java.util.List;
46  import java.util.Map;
47  import java.util.Properties;
48  import java.util.jar.Manifest;
49  import java.util.zip.Deflater;
50  
51  import static java.nio.charset.StandardCharsets.UTF_8;
52  import static java.nio.file.Files.isDirectory;
53  import static org.apache.maven.plugin.surefire.SurefireHelper.escapeToPlatformPath;
54  import static org.apache.maven.plugin.surefire.booterclient.JarManifestForkConfiguration.ClasspathElementUri.absolute;
55  import static org.apache.maven.plugin.surefire.booterclient.JarManifestForkConfiguration.ClasspathElementUri.relative;
56  import static org.apache.maven.surefire.util.internal.StringUtils.NL;
57  
58  /**
59   * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
60   * @since 2.21.0.Jigsaw
61   */
62  public final class JarManifestForkConfiguration
63      extends AbstractClasspathForkConfiguration
64  {
65      @SuppressWarnings( "checkstyle:parameternumber" )
66      public JarManifestForkConfiguration( @Nonnull Classpath bootClasspath, @Nonnull File tempDirectory,
67                                           @Nullable String debugLine, @Nonnull File workingDirectory,
68                                           @Nonnull Properties modelProperties, @Nullable String argLine,
69                                           @Nonnull Map<String, String> environmentVariables,
70                                           @Nonnull String[] excludedEnvironmentVariables,
71                                           boolean debug,
72                                           int forkCount, boolean reuseForks, @Nonnull Platform pluginPlatform,
73                                           @Nonnull ConsoleLogger log )
74      {
75          super( bootClasspath, tempDirectory, debugLine, workingDirectory, modelProperties, argLine,
76                  environmentVariables, excludedEnvironmentVariables, debug, forkCount, reuseForks, pluginPlatform, log );
77      }
78  
79      @Override
80      protected void resolveClasspath( @Nonnull OutputStreamFlushableCommandline cli,
81                                       @Nonnull String booterThatHasMainMethod,
82                                       @Nonnull StartupConfiguration config,
83                                       @Nonnull File dumpLogDirectory )
84              throws SurefireBooterForkException
85      {
86          try
87          {
88              File jar = createJar( toCompleteClasspath( config ), booterThatHasMainMethod, dumpLogDirectory );
89              cli.createArg().setValue( "-jar" );
90              cli.createArg().setValue( escapeToPlatformPath( jar.getAbsolutePath() ) );
91          }
92          catch ( IOException e )
93          {
94              throw new SurefireBooterForkException( "Error creating archive file", e );
95          }
96      }
97  
98      /**
99       * Create a jar with just a manifest containing a Main-Class entry for BooterConfiguration and a Class-Path entry
100      * for all classpath elements.
101      *
102      * @param classPath      List&lt;String&gt; of all classpath elements.
103      * @param startClassName The class name to start (main-class)
104      * @return file of the jar
105      * @throws IOException When a file operation fails.
106      */
107     @Nonnull
108     private File createJar( @Nonnull List<String> classPath, @Nonnull String startClassName,
109                             @Nonnull File dumpLogDirectory )
110             throws IOException
111     {
112         File file = File.createTempFile( "surefirebooter", ".jar", getTempDirectory() );
113         if ( !isDebug() )
114         {
115             file.deleteOnExit();
116         }
117         Path parent = file.getParentFile().toPath();
118         OutputStream fos = new BufferedOutputStream( new FileOutputStream( file ), 64 * 1024 );
119 
120         try ( ZipArchiveOutputStream zos = new ZipArchiveOutputStream( fos ) )
121         {
122             zos.setUseZip64( Zip64Mode.Never );
123             zos.setLevel( Deflater.NO_COMPRESSION );
124 
125             ZipArchiveEntry ze = new ZipArchiveEntry( "META-INF/MANIFEST.MF" );
126             zos.putArchiveEntry( ze );
127 
128             Manifest man = new Manifest();
129 
130             boolean dumpError = true;
131 
132             // we can't use StringUtils.join here since we need to add a '/' to
133             // the end of directory entries - otherwise the jvm will ignore them.
134             StringBuilder cp = new StringBuilder();
135             for ( Iterator<String> it = classPath.iterator(); it.hasNext(); )
136             {
137                 Path classPathElement = Paths.get( it.next() );
138                 ClasspathElementUri classpathElementUri =
139                         toClasspathElementUri( parent, classPathElement, dumpLogDirectory, dumpError );
140                 // too many errors in dump file with the same root cause may slow down the Boot Manifest-JAR startup
141                 dumpError &= !classpathElementUri.absolute;
142                 cp.append( classpathElementUri.uri );
143                 if ( isDirectory( classPathElement ) && !classpathElementUri.uri.endsWith( "/" ) )
144                 {
145                     cp.append( '/' );
146                 }
147 
148                 if ( it.hasNext() )
149                 {
150                     cp.append( ' ' );
151                 }
152             }
153 
154             man.getMainAttributes().putValue( "Manifest-Version", "1.0" );
155             man.getMainAttributes().putValue( "Class-Path", cp.toString().trim() );
156             man.getMainAttributes().putValue( "Main-Class", startClassName );
157 
158             man.write( zos );
159 
160             zos.closeArchiveEntry();
161 
162             return file;
163         }
164     }
165 
166     static String relativize( @Nonnull Path parent, @Nonnull Path child )
167             throws IllegalArgumentException
168     {
169         return parent.relativize( child )
170                 .toString();
171     }
172 
173     static String toAbsoluteUri( @Nonnull Path absolutePath )
174     {
175         return absolutePath.toUri()
176                 .toASCIIString();
177     }
178 
179     static ClasspathElementUri toClasspathElementUri( @Nonnull Path parent,
180                                          @Nonnull Path classPathElement,
181                                          @Nonnull File dumpLogDirectory,
182                                          boolean dumpError )
183     {
184         try
185         {
186             String relativePath = relativize( parent, classPathElement );
187             return relative( escapeUri( relativePath, UTF_8 ) );
188         }
189         catch ( IllegalArgumentException e )
190         {
191             if ( dumpError )
192             {
193                 String error = "Boot Manifest-JAR contains absolute paths in classpath '"
194                         + classPathElement
195                         + "'"
196                         + NL
197                         + "Hint: <argLine>-Djdk.net.URLClassPath.disableClassPathURLCheck=true</argLine>";
198                 InPluginProcessDumpSingleton.getSingleton()
199                         .dumpStreamText( error, dumpLogDirectory );
200             }
201 
202             return absolute( toAbsoluteUri( classPathElement ) );
203         }
204     }
205 
206     static final class ClasspathElementUri
207     {
208         final String uri;
209         final boolean absolute;
210 
211         private ClasspathElementUri( String uri, boolean absolute )
212         {
213             this.uri = uri;
214             this.absolute = absolute;
215         }
216 
217         static ClasspathElementUri absolute( String uri )
218         {
219             return new ClasspathElementUri( uri, true );
220         }
221 
222         static ClasspathElementUri relative( String uri )
223         {
224             return new ClasspathElementUri( uri, false );
225         }
226     }
227 
228     static String escapeUri( String input, Charset encoding )
229     {
230         try
231         {
232             String uriFormEncoded = URLEncoder.encode( input, encoding.name() );
233 
234             String uriPathEncoded = uriFormEncoded.replaceAll( "\\+", "%20" );
235             uriPathEncoded = uriPathEncoded.replaceAll( "%2F|%5C", "/" );
236 
237             return uriPathEncoded;
238         }
239         catch ( UnsupportedEncodingException e )
240         {
241             throw new IllegalStateException( "avoided by using Charset" );
242         }
243     }
244 }