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.util.Iterator;
38  import java.util.List;
39  import java.util.Map;
40  import java.util.Properties;
41  import java.util.jar.JarEntry;
42  import java.util.jar.JarOutputStream;
43  import java.util.jar.Manifest;
44  
45  import static org.apache.maven.plugin.surefire.SurefireHelper.escapeToPlatformPath;
46  
47  /**
48   * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
49   * @since 2.21.0.Jigsaw
50   */
51  public final class JarManifestForkConfiguration
52      extends AbstractClasspathForkConfiguration
53  {
54      @SuppressWarnings( "checkstyle:parameternumber" )
55      public JarManifestForkConfiguration( @Nonnull Classpath bootClasspath, @Nonnull File tempDirectory,
56                                           @Nullable String debugLine, @Nonnull File workingDirectory,
57                                           @Nonnull Properties modelProperties, @Nullable String argLine,
58                                           @Nonnull Map<String, String> environmentVariables, boolean debug,
59                                           int forkCount, boolean reuseForks, @Nonnull Platform pluginPlatform,
60                                           @Nonnull ConsoleLogger log )
61      {
62          super( bootClasspath, tempDirectory, debugLine, workingDirectory, modelProperties, argLine,
63                  environmentVariables, debug, forkCount, reuseForks, pluginPlatform, log );
64      }
65  
66      @Override
67      protected void resolveClasspath( @Nonnull OutputStreamFlushableCommandline cli,
68                                       @Nonnull String booterThatHasMainMethod,
69                                       @Nonnull StartupConfiguration config,
70                                       @Nonnull File dumpLogDirectory )
71              throws SurefireBooterForkException
72      {
73          try
74          {
75              File jar = createJar( toCompleteClasspath( config ), booterThatHasMainMethod, dumpLogDirectory );
76              cli.createArg().setValue( "-jar" );
77              cli.createArg().setValue( escapeToPlatformPath( jar.getAbsolutePath() ) );
78          }
79          catch ( IOException e )
80          {
81              throw new SurefireBooterForkException( "Error creating archive file", e );
82          }
83      }
84  
85      /**
86       * Create a jar with just a manifest containing a Main-Class entry for BooterConfiguration and a Class-Path entry
87       * for all classpath elements.
88       *
89       * @param classPath      List&lt;String&gt; of all classpath elements.
90       * @param startClassName The class name to start (main-class)
91       * @return file of the jar
92       * @throws IOException When a file operation fails.
93       */
94      @Nonnull
95      private File createJar( @Nonnull List<String> classPath, @Nonnull String startClassName,
96                              @Nonnull File dumpLogDirectory )
97              throws IOException
98      {
99          File file = File.createTempFile( "surefirebooter", ".jar", getTempDirectory() );
100         if ( !isDebug() )
101         {
102             file.deleteOnExit();
103         }
104         Path parent = file.getParentFile().toPath();
105         FileOutputStream fos = new FileOutputStream( file );
106         try ( JarOutputStream jos = new JarOutputStream( fos ) )
107         {
108             jos.setLevel( JarOutputStream.STORED );
109             JarEntry je = new JarEntry( "META-INF/MANIFEST.MF" );
110             jos.putNextEntry( je );
111 
112             Manifest man = new Manifest();
113 
114             // we can't use StringUtils.join here since we need to add a '/' to
115             // the end of directory entries - otherwise the jvm will ignore them.
116             StringBuilder cp = new StringBuilder();
117             for ( Iterator<String> it = classPath.iterator(); it.hasNext(); )
118             {
119                 File classPathElement = new File( it.next() );
120                 String uri = toClasspathElementUri( parent, classPathElement, dumpLogDirectory );
121                 cp.append( uri );
122                 if ( classPathElement.isDirectory() && !uri.endsWith( "/" ) )
123                 {
124                     cp.append( '/' );
125                 }
126 
127                 if ( it.hasNext() )
128                 {
129                     cp.append( ' ' );
130                 }
131             }
132 
133             man.getMainAttributes().putValue( "Manifest-Version", "1.0" );
134             man.getMainAttributes().putValue( "Class-Path", cp.toString().trim() );
135             man.getMainAttributes().putValue( "Main-Class", startClassName );
136 
137             man.write( jos );
138 
139             jos.closeEntry();
140             jos.flush();
141 
142             return file;
143         }
144     }
145 
146     private static String toClasspathElementUri( @Nonnull Path parent,
147                                                  @Nonnull File classPathElement,
148                                                  @Nonnull File dumpLogDirectory )
149             throws IOException
150     {
151         try
152         {
153             return new URI( null, parent.relativize( classPathElement.toPath() ).toString(), null )
154                     .toASCIIString();
155         }
156         catch ( IllegalArgumentException e )
157         {
158             String error = "Boot Manifest-JAR contains absolute paths in classpath " + classPathElement.getPath();
159             InPluginProcessDumpSingleton.getSingleton()
160                     .dumpException( e, error, dumpLogDirectory );
161 
162             return classPathElement.toURI()
163                     .toASCIIString();
164         }
165         catch ( URISyntaxException e )
166         {
167             // This is really unexpected, so fail
168             throw new IOException( "Could not create a relative path "
169                     + classPathElement
170                     + " against "
171                     + parent, e );
172         }
173     }
174 }