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