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