View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.plugin.surefire.booterclient;
20  
21  import javax.annotation.Nonnull;
22  import javax.annotation.Nullable;
23  
24  import java.io.BufferedOutputStream;
25  import java.io.File;
26  import java.io.FileOutputStream;
27  import java.io.IOException;
28  import java.io.OutputStream;
29  import java.io.UnsupportedEncodingException;
30  import java.net.URLEncoder;
31  import java.nio.charset.Charset;
32  import java.nio.file.Path;
33  import java.nio.file.Paths;
34  import java.util.Iterator;
35  import java.util.List;
36  import java.util.Map;
37  import java.util.Properties;
38  import java.util.jar.Manifest;
39  import java.util.zip.Deflater;
40  
41  import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.Commandline;
42  import org.apache.maven.plugin.surefire.booterclient.output.InPluginProcessDumpSingleton;
43  import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
44  import org.apache.maven.surefire.api.util.TempFileManager;
45  import org.apache.maven.surefire.booter.Classpath;
46  import org.apache.maven.surefire.booter.StartupConfiguration;
47  import org.apache.maven.surefire.booter.SurefireBooterForkException;
48  import org.apache.maven.surefire.extensions.ForkNodeFactory;
49  import org.apache.maven.surefire.shared.compress.archivers.zip.Zip64Mode;
50  import org.apache.maven.surefire.shared.compress.archivers.zip.ZipArchiveEntry;
51  import org.apache.maven.surefire.shared.compress.archivers.zip.ZipArchiveOutputStream;
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.api.util.internal.StringUtils.NL;
59  import static org.apache.maven.surefire.shared.utils.StringUtils.isNotBlank;
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 extends AbstractClasspathForkConfiguration {
66      @SuppressWarnings("checkstyle:parameternumber")
67      public JarManifestForkConfiguration(
68              @Nonnull Classpath bootClasspath,
69              @Nonnull File tempDirectory,
70              @Nullable String debugLine,
71              @Nonnull File workingDirectory,
72              @Nonnull Properties modelProperties,
73              @Nullable String argLine,
74              @Nonnull Map<String, String> environmentVariables,
75              @Nonnull String[] excludedEnvironmentVariables,
76              boolean debug,
77              int forkCount,
78              boolean reuseForks,
79              @Nonnull Platform pluginPlatform,
80              @Nonnull ConsoleLogger log,
81              @Nonnull ForkNodeFactory forkNodeFactory) {
82          super(
83                  bootClasspath,
84                  tempDirectory,
85                  debugLine,
86                  workingDirectory,
87                  modelProperties,
88                  argLine,
89                  environmentVariables,
90                  excludedEnvironmentVariables,
91                  debug,
92                  forkCount,
93                  reuseForks,
94                  pluginPlatform,
95                  log,
96                  forkNodeFactory);
97      }
98  
99      @Override
100     protected void resolveClasspath(
101             @Nonnull Commandline cli,
102             @Nonnull String booterThatHasMainMethod,
103             @Nonnull StartupConfiguration config,
104             @Nonnull File dumpLogDirectory)
105             throws SurefireBooterForkException {
106         try {
107             File jar = createJar(toCompleteClasspath(config), booterThatHasMainMethod, dumpLogDirectory);
108             cli.createArg().setValue("-jar");
109             cli.createArg().setValue(escapeToPlatformPath(jar.getAbsolutePath()));
110         } catch (IOException e) {
111             throw new SurefireBooterForkException("Error creating archive file", e);
112         }
113     }
114 
115     /**
116      * Create a jar with just a manifest containing a Main-Class entry for BooterConfiguration and a Class-Path entry
117      * for all classpath elements.
118      *
119      * @param classPath      List&lt;String&gt; of all classpath elements.
120      * @param startClassName The class name to start (main-class)
121      * @return file of the jar
122      * @throws IOException When a file operation fails.
123      */
124     @Nonnull
125     private File createJar(
126             @Nonnull List<String> classPath, @Nonnull String startClassName, @Nonnull File dumpLogDirectory)
127             throws IOException {
128         File file = TempFileManager.instance(getTempDirectory()).createTempFile("surefirebooter", ".jar");
129         if (!isDebug()) {
130             file.deleteOnExit();
131         }
132         Path parent = file.getParentFile().toPath();
133         OutputStream fos = new BufferedOutputStream(new FileOutputStream(file), 64 * 1024);
134 
135         try (ZipArchiveOutputStream zos = new ZipArchiveOutputStream(fos)) {
136             zos.setUseZip64(Zip64Mode.Never);
137             zos.setLevel(Deflater.NO_COMPRESSION);
138 
139             ZipArchiveEntry ze = new ZipArchiveEntry("META-INF/MANIFEST.MF");
140             zos.putArchiveEntry(ze);
141 
142             Manifest man = new Manifest();
143 
144             boolean dumpError = true;
145 
146             // we can't use StringUtils.join here since we need to add a '/' to
147             // the end of directory entries - otherwise the jvm will ignore them.
148             StringBuilder cp = new StringBuilder();
149             for (Iterator<String> it = classPath.iterator(); it.hasNext(); ) {
150                 Path classPathElement = Paths.get(it.next());
151                 ClasspathElementUri classpathElementUri =
152                         toClasspathElementUri(parent, classPathElement, dumpLogDirectory, dumpError);
153                 // too many errors in dump file with the same root cause may slow down the Boot Manifest-JAR startup
154                 dumpError &= !classpathElementUri.absolute;
155                 cp.append(classpathElementUri.uri);
156                 if (isDirectory(classPathElement) && !classpathElementUri.uri.endsWith("/")) {
157                     cp.append('/');
158                 }
159 
160                 if (it.hasNext()) {
161                     cp.append(' ');
162                 }
163             }
164 
165             man.getMainAttributes().putValue("Manifest-Version", "1.0");
166             man.getMainAttributes().putValue("Class-Path", cp.toString().trim());
167             man.getMainAttributes().putValue("Main-Class", startClassName);
168 
169             man.write(zos);
170 
171             zos.closeArchiveEntry();
172 
173             return file;
174         }
175     }
176 
177     static String relativize(@Nonnull Path parent, @Nonnull Path child) throws IllegalArgumentException {
178         return parent.relativize(child).toString();
179     }
180 
181     static String toAbsoluteUri(@Nonnull Path absolutePath) {
182         // UNC paths need to be written as file:////
183         // see https://bugs.openjdk.org/browse/JDK-8320760
184         if (absolutePath.toString().startsWith("\\\\")) {
185             return absolutePath.toFile().toURI().toASCIIString();
186         }
187         return absolutePath.toUri().toASCIIString();
188     }
189 
190     static ClasspathElementUri toClasspathElementUri(
191             @Nonnull Path parent, @Nonnull Path classPathElement, @Nonnull File dumpLogDirectory, boolean dumpError) {
192         try {
193             String relativePath = relativize(parent, classPathElement);
194             return relative(escapeUri(relativePath, UTF_8));
195         } catch (IllegalArgumentException e) {
196             if (dumpError) {
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                     error += NL;
205                     error += e.getLocalizedMessage();
206                 }
207 
208                 InPluginProcessDumpSingleton.getSingleton().dumpStreamText(error, dumpLogDirectory);
209             }
210 
211             return absolute(toAbsoluteUri(classPathElement));
212         }
213     }
214 
215     static final class ClasspathElementUri {
216         final String uri;
217         final boolean absolute;
218 
219         private ClasspathElementUri(String uri, boolean absolute) {
220             this.uri = uri;
221             this.absolute = absolute;
222         }
223 
224         static ClasspathElementUri absolute(String uri) {
225             return new ClasspathElementUri(uri, true);
226         }
227 
228         static ClasspathElementUri relative(String uri) {
229             return new ClasspathElementUri(uri, false);
230         }
231     }
232 
233     static String escapeUri(String input, Charset encoding) {
234         try {
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         } catch (UnsupportedEncodingException e) {
242             throw new IllegalStateException("avoided by using Charset");
243         }
244     }
245 }