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