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         return absolutePath.toUri().toASCIIString();
183     }
184 
185     static ClasspathElementUri toClasspathElementUri(
186             @Nonnull Path parent, @Nonnull Path classPathElement, @Nonnull File dumpLogDirectory, boolean dumpError) {
187         try {
188             String relativePath = relativize(parent, classPathElement);
189             return relative(escapeUri(relativePath, UTF_8));
190         } catch (IllegalArgumentException e) {
191             if (dumpError) {
192                 String error = "Boot Manifest-JAR contains absolute paths in classpath '"
193                         + classPathElement
194                         + "'"
195                         + NL
196                         + "Hint: <argLine>-Djdk.net.URLClassPath.disableClassPathURLCheck=true</argLine>";
197 
198                 if (isNotBlank(e.getLocalizedMessage())) {
199                     error += NL;
200                     error += e.getLocalizedMessage();
201                 }
202 
203                 InPluginProcessDumpSingleton.getSingleton().dumpStreamText(error, dumpLogDirectory);
204             }
205 
206             return absolute(toAbsoluteUri(classPathElement));
207         }
208     }
209 
210     static final class ClasspathElementUri {
211         final String uri;
212         final boolean absolute;
213 
214         private ClasspathElementUri(String uri, boolean absolute) {
215             this.uri = uri;
216             this.absolute = absolute;
217         }
218 
219         static ClasspathElementUri absolute(String uri) {
220             return new ClasspathElementUri(uri, true);
221         }
222 
223         static ClasspathElementUri relative(String uri) {
224             return new ClasspathElementUri(uri, false);
225         }
226     }
227 
228     static String escapeUri(String input, Charset encoding) {
229         try {
230             String uriFormEncoded = URLEncoder.encode(input, encoding.name());
231 
232             String uriPathEncoded = uriFormEncoded.replaceAll("\\+", "%20");
233             uriPathEncoded = uriPathEncoded.replaceAll("%2F|%5C", "/");
234 
235             return uriPathEncoded;
236         } catch (UnsupportedEncodingException e) {
237             throw new IllegalStateException("avoided by using Charset");
238         }
239     }
240 }