7 changed files with 307 additions and 351 deletions
@ -0,0 +1,143 @@
@@ -0,0 +1,143 @@
|
||||
/* |
||||
* Copyright 2012-2025 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.boot.maven; |
||||
|
||||
import java.io.File; |
||||
import java.io.IOException; |
||||
import java.net.URISyntaxException; |
||||
import java.net.URL; |
||||
import java.nio.charset.Charset; |
||||
import java.nio.charset.UnsupportedCharsetException; |
||||
import java.nio.file.Files; |
||||
import java.nio.file.Path; |
||||
import java.nio.file.Paths; |
||||
import java.util.Arrays; |
||||
import java.util.Collections; |
||||
import java.util.List; |
||||
import java.util.Locale; |
||||
import java.util.function.UnaryOperator; |
||||
import java.util.stream.Collector; |
||||
import java.util.stream.Collectors; |
||||
|
||||
import org.springframework.util.StringUtils; |
||||
|
||||
/** |
||||
* Encapsulates a class path and allows argument parameters to be created. On Windows an |
||||
* argument file is used whenever possible since the maximum command line length is |
||||
* limited. |
||||
* |
||||
* @author Stephane Nicoll |
||||
* @author Dmytro Nosan |
||||
* @author Phillip Webb |
||||
*/ |
||||
final class ClassPath { |
||||
|
||||
private static final Collector<CharSequence, ?, String> JOIN_BY_PATH_SEPARATOR = Collectors |
||||
.joining(File.pathSeparator); |
||||
|
||||
private final boolean preferArgFile; |
||||
|
||||
private final String path; |
||||
|
||||
private ClassPath(boolean preferArgFile, String path) { |
||||
this.preferArgFile = preferArgFile; |
||||
this.path = path; |
||||
} |
||||
|
||||
/** |
||||
* Return the args to append to a java command line call (including {@code -cp}). |
||||
* @param allowArgFile if an arg file can be used |
||||
* @return the command line arguments |
||||
*/ |
||||
List<String> args(boolean allowArgFile) { |
||||
return (!this.path.isEmpty()) ? List.of("-cp", classPathArg(allowArgFile)) : Collections.emptyList(); |
||||
} |
||||
|
||||
private String classPathArg(boolean allowArgFile) { |
||||
if (this.preferArgFile && allowArgFile) { |
||||
try { |
||||
return "@" + createArgFile(); |
||||
} |
||||
catch (IOException ex) { |
||||
return this.path; |
||||
} |
||||
} |
||||
return this.path; |
||||
} |
||||
|
||||
private Path createArgFile() throws IOException { |
||||
Path argFile = Files.createTempFile("spring-boot-", ".argfile"); |
||||
argFile.toFile().deleteOnExit(); |
||||
Files.writeString(argFile, "\"" + this.path.replace("\\", "\\\\") + "\"", charset()); |
||||
return argFile; |
||||
} |
||||
|
||||
private Charset charset() { |
||||
try { |
||||
String nativeEncoding = System.getProperty("native.encoding"); |
||||
return (nativeEncoding != null) ? Charset.forName(nativeEncoding) : Charset.defaultCharset(); |
||||
} |
||||
catch (UnsupportedCharsetException ex) { |
||||
return Charset.defaultCharset(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Factory method to create a {@link ClassPath} of the given URLs. |
||||
* @param urls the class path URLs |
||||
* @return a new {@link ClassPath} instance |
||||
*/ |
||||
static ClassPath of(URL... urls) { |
||||
return of(Arrays.asList(urls)); |
||||
} |
||||
|
||||
/** |
||||
* Factory method to create a {@link ClassPath} of the given URLs. |
||||
* @param urls the class path URLs |
||||
* @return a new {@link ClassPath} instance |
||||
*/ |
||||
static ClassPath of(List<URL> urls) { |
||||
return of(System::getProperty, urls); |
||||
} |
||||
|
||||
/** |
||||
* Factory method to create a {@link ClassPath} of the given URLs. |
||||
* @param getSystemProperty {@link UnaryOperator} allowing access to system properties |
||||
* @param urls the class path URLs |
||||
* @return a new {@link ClassPath} instance |
||||
*/ |
||||
static ClassPath of(UnaryOperator<String> getSystemProperty, List<URL> urls) { |
||||
boolean preferrArgFile = urls.size() > 1 && isWindows(getSystemProperty); |
||||
return new ClassPath(preferrArgFile, |
||||
urls.stream().map(ClassPath::toPathString).collect(JOIN_BY_PATH_SEPARATOR)); |
||||
} |
||||
|
||||
private static boolean isWindows(UnaryOperator<String> getSystemProperty) { |
||||
String os = getSystemProperty.apply("os.name"); |
||||
return StringUtils.hasText(os) && os.toLowerCase(Locale.ROOT).contains("win"); |
||||
} |
||||
|
||||
private static String toPathString(URL url) { |
||||
try { |
||||
return Paths.get(url.toURI()).toString(); |
||||
} |
||||
catch (URISyntaxException ex) { |
||||
throw new IllegalArgumentException(ex); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -1,196 +0,0 @@
@@ -1,196 +0,0 @@
|
||||
/* |
||||
* Copyright 2012-2025 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.boot.maven; |
||||
|
||||
import java.io.File; |
||||
import java.io.IOException; |
||||
import java.net.URISyntaxException; |
||||
import java.net.URL; |
||||
import java.nio.charset.Charset; |
||||
import java.nio.charset.UnsupportedCharsetException; |
||||
import java.nio.file.Files; |
||||
import java.nio.file.Path; |
||||
import java.nio.file.Paths; |
||||
import java.util.ArrayList; |
||||
import java.util.Arrays; |
||||
import java.util.Collections; |
||||
import java.util.List; |
||||
import java.util.Locale; |
||||
import java.util.stream.Collectors; |
||||
import java.util.stream.Stream; |
||||
|
||||
import org.springframework.util.ObjectUtils; |
||||
import org.springframework.util.StringUtils; |
||||
|
||||
/** |
||||
* Helper class to build the -cp (classpath) argument of a java process. |
||||
* |
||||
* @author Stephane Nicoll |
||||
* @author Dmytro Nosan |
||||
*/ |
||||
class ClasspathBuilder { |
||||
|
||||
private final List<URL> urls; |
||||
|
||||
protected ClasspathBuilder(List<URL> urls) { |
||||
this.urls = urls; |
||||
} |
||||
|
||||
/** |
||||
* Creates a ClasspathBuilder instance using the specified list of URLs. |
||||
* @param urls a list of {@link URL} objects representing the elements of the |
||||
* classpath |
||||
* @return a new instance of {@code ClasspathBuilder} |
||||
*/ |
||||
static ClasspathBuilder forURLs(List<URL> urls) { |
||||
return new ClasspathBuilder(new ArrayList<>(urls)); |
||||
} |
||||
|
||||
/** |
||||
* Creates a ClasspathBuilder instance using the specified array of URLs. |
||||
* @param urls an array of {@link URL} objects representing the elements of the |
||||
* classpath |
||||
* @return a new instance of {@code ClasspathBuilder} |
||||
*/ |
||||
static ClasspathBuilder forURLs(URL... urls) { |
||||
return new ClasspathBuilder(Arrays.asList(urls)); |
||||
} |
||||
|
||||
/** |
||||
* Builds {@link Classpath} that containing a classpath argument and its corresponding |
||||
* classpath elements. |
||||
* @return a {@code Classpath} |
||||
*/ |
||||
Classpath build() { |
||||
if (ObjectUtils.isEmpty(this.urls)) { |
||||
return new Classpath("", Collections.emptyList()); |
||||
} |
||||
if (this.urls.size() == 1) { |
||||
Path file = toFile(this.urls.get(0)); |
||||
return new Classpath(file.toString(), List.of(file)); |
||||
} |
||||
List<Path> files = this.urls.stream().map(ClasspathBuilder::toFile).toList(); |
||||
String argument = files.stream().map(Object::toString).collect(Collectors.joining(File.pathSeparator)); |
||||
if (needsClasspathArgFile()) { |
||||
argument = createArgFile(argument); |
||||
} |
||||
return new Classpath(argument, files); |
||||
} |
||||
|
||||
/** |
||||
* Determines if an argument file should be used for the classpath based on the |
||||
* operating system. On Windows, argument files are used due to the command length |
||||
* limitation. |
||||
* @return {@code true} if an argument file is required for the classpath, |
||||
* {@code false} otherwise |
||||
*/ |
||||
protected boolean needsClasspathArgFile() { |
||||
String os = System.getProperty("os.name"); |
||||
if (!StringUtils.hasText(os)) { |
||||
return false; |
||||
} |
||||
// Windows limits the maximum command length, so we use an argfile
|
||||
return os.toLowerCase(Locale.ROOT).contains("win"); |
||||
} |
||||
|
||||
/** |
||||
* Create a temporary file with the given {@code} classpath. Return a suitable |
||||
* argument to load the file, that is the full path prefixed by {@code @}. |
||||
* @param classpath the classpath to use |
||||
* @return a suitable argument for the classpath using a file |
||||
*/ |
||||
private String createArgFile(String classpath) { |
||||
try { |
||||
return "@" + writeClasspathToFile(classpath); |
||||
} |
||||
catch (IOException ex) { |
||||
return classpath; |
||||
} |
||||
} |
||||
|
||||
private Path writeClasspathToFile(CharSequence classpath) throws IOException { |
||||
Path tempFile = Files.createTempFile("spring-boot-", ".argfile"); |
||||
tempFile.toFile().deleteOnExit(); |
||||
Files.writeString(tempFile, "\"" + escape(classpath) + "\"", getCharset()); |
||||
return tempFile; |
||||
} |
||||
|
||||
private static Charset getCharset() { |
||||
String nativeEncoding = System.getProperty("native.encoding"); |
||||
if (nativeEncoding == null) { |
||||
return Charset.defaultCharset(); |
||||
} |
||||
try { |
||||
return Charset.forName(nativeEncoding); |
||||
} |
||||
catch (UnsupportedCharsetException ex) { |
||||
return Charset.defaultCharset(); |
||||
} |
||||
} |
||||
|
||||
private static String escape(CharSequence content) { |
||||
return content.toString().replace("\\", "\\\\"); |
||||
} |
||||
|
||||
private static Path toFile(URL url) { |
||||
try { |
||||
return Paths.get(url.toURI()); |
||||
} |
||||
catch (URISyntaxException ex) { |
||||
throw new IllegalArgumentException(ex); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Classpath consisting of a {@code -cp} argument and its associated elements. |
||||
*/ |
||||
static final class Classpath { |
||||
|
||||
private final String argument; |
||||
|
||||
private final List<Path> elements; |
||||
|
||||
private Classpath(String argument, List<Path> elements) { |
||||
this.argument = argument; |
||||
this.elements = elements; |
||||
} |
||||
|
||||
/** |
||||
* Return the {@code -cp} argument value; on Windows, the path to an argument file |
||||
* is returned, prefixed with '@'. |
||||
* @return the argument to use |
||||
*/ |
||||
String argument() { |
||||
return this.argument; |
||||
} |
||||
|
||||
/** |
||||
* Return the classpath elements. |
||||
* @return the JAR files to use |
||||
*/ |
||||
Stream<Path> elements() { |
||||
return this.elements.stream(); |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return elements().map(Path::toString).collect(Collectors.joining(File.pathSeparator)); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,96 @@
@@ -0,0 +1,96 @@
|
||||
/* |
||||
* Copyright 2012-2025 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.boot.maven; |
||||
|
||||
import java.io.File; |
||||
import java.nio.file.Path; |
||||
import java.nio.file.Paths; |
||||
import java.util.Collections; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.function.UnaryOperator; |
||||
|
||||
import org.junit.jupiter.api.Test; |
||||
import org.junit.jupiter.api.io.TempDir; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
/** |
||||
* Test for {@link ClassPath}. |
||||
* |
||||
* @author Dmytro Nosan |
||||
* @author Stephane Nicoll |
||||
* @author Phillip Webb |
||||
*/ |
||||
class ClassPathTests { |
||||
|
||||
@Test |
||||
void argsWhenNoClassPathReturnsEmptyList() { |
||||
assertThat(ClassPath.of(Collections.emptyList()).args(false)).isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void argsWhenSingleUrlOnWindowsUsesPath(@TempDir Path temp) throws Exception { |
||||
Path path = temp.resolve("test.jar"); |
||||
ClassPath classPath = ClassPath.of(onWindows(), List.of(path.toUri().toURL())); |
||||
assertThat(classPath.args(true)).containsExactly("-cp", path.toString()); |
||||
} |
||||
|
||||
@Test |
||||
void argsWhenSingleUrlNotOnWindowsUsesPath(@TempDir Path temp) throws Exception { |
||||
Path path = temp.resolve("test.jar"); |
||||
ClassPath classPath = ClassPath.of(onLinux(), List.of(path.toUri().toURL())); |
||||
assertThat(classPath.args(true)).containsExactly("-cp", path.toString()); |
||||
} |
||||
|
||||
@Test |
||||
void argsWhenMultipleUrlsOnWindowsAndAllowedUsesArgFile(@TempDir Path temp) throws Exception { |
||||
Path path1 = temp.resolve("test1.jar"); |
||||
Path path2 = temp.resolve("test2.jar"); |
||||
ClassPath classPath = ClassPath.of(onWindows(), List.of(path1.toUri().toURL(), path2.toUri().toURL())); |
||||
List<String> args = classPath.args(true); |
||||
assertThat(args.get(0)).isEqualTo("-cp"); |
||||
assertThat(args.get(1)).startsWith("@"); |
||||
assertThat(Paths.get(args.get(1).substring(1))) |
||||
.hasContent("\"" + (path1 + File.pathSeparator + path2).replace("\\", "\\\\") + "\""); |
||||
} |
||||
|
||||
@Test |
||||
void argsWhenMultipleUrlsOnWindowsAndNotAllowedUsesPath(@TempDir Path temp) throws Exception { |
||||
Path path1 = temp.resolve("test1.jar"); |
||||
Path path2 = temp.resolve("test2.jar"); |
||||
ClassPath classPath = ClassPath.of(onWindows(), List.of(path1.toUri().toURL(), path2.toUri().toURL())); |
||||
assertThat(classPath.args(false)).containsExactly("-cp", path1 + File.pathSeparator + path2); |
||||
} |
||||
|
||||
@Test |
||||
void argsWhenMultipleUrlsNotOnWindowsUsesPath(@TempDir Path temp) throws Exception { |
||||
Path path1 = temp.resolve("test1.jar"); |
||||
Path path2 = temp.resolve("test2.jar"); |
||||
ClassPath classPath = ClassPath.of(onLinux(), List.of(path1.toUri().toURL(), path2.toUri().toURL())); |
||||
assertThat(classPath.args(true)).containsExactly("-cp", path1 + File.pathSeparator + path2); |
||||
} |
||||
|
||||
private UnaryOperator<String> onWindows() { |
||||
return Map.of("os.name", "windows")::get; |
||||
} |
||||
|
||||
private UnaryOperator<String> onLinux() { |
||||
return Map.of("os.name", "linux")::get; |
||||
} |
||||
|
||||
} |
||||
@ -1,102 +0,0 @@
@@ -1,102 +0,0 @@
|
||||
/* |
||||
* Copyright 2012-2025 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.boot.maven; |
||||
|
||||
import java.io.File; |
||||
import java.nio.file.Path; |
||||
import java.nio.file.Paths; |
||||
|
||||
import org.junit.jupiter.api.Nested; |
||||
import org.junit.jupiter.api.Test; |
||||
import org.junit.jupiter.api.condition.DisabledOnOs; |
||||
import org.junit.jupiter.api.condition.EnabledOnOs; |
||||
import org.junit.jupiter.api.condition.OS; |
||||
import org.junit.jupiter.api.io.TempDir; |
||||
|
||||
import org.springframework.boot.maven.ClasspathBuilder.Classpath; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
/** |
||||
* Tests for {@link ClasspathBuilder}. |
||||
* |
||||
* @author Dmytro Nosan |
||||
* @author Stephane Nicoll |
||||
*/ |
||||
class ClasspathBuilderTests { |
||||
|
||||
@Nested |
||||
@EnabledOnOs(OS.WINDOWS) |
||||
class WindowsTests { |
||||
|
||||
@Test |
||||
void buildWithEmptyClassPath() { |
||||
Classpath classpath = ClasspathBuilder.forURLs().build(); |
||||
assertThat(classpath.argument()).isEmpty(); |
||||
assertThat(classpath.elements()).isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void buildWithSingleClassPathURL(@TempDir Path tempDir) throws Exception { |
||||
Path file = tempDir.resolve("test.jar"); |
||||
Classpath classpath = ClasspathBuilder.forURLs(file.toUri().toURL()).build(); |
||||
assertThat(classpath.argument()).isEqualTo(file.toString()); |
||||
assertThat(classpath.elements()).singleElement().isEqualTo(file); |
||||
} |
||||
|
||||
@Test |
||||
void buildWithMultipleClassPathURLs(@TempDir Path tempDir) throws Exception { |
||||
Path file = tempDir.resolve("test.jar"); |
||||
Path file1 = tempDir.resolve("test1.jar"); |
||||
String classpath = ClasspathBuilder.forURLs(file.toUri().toURL(), file1.toUri().toURL()).build().argument(); |
||||
assertThat(classpath).startsWith("@"); |
||||
assertThat(Paths.get(classpath.substring(1))) |
||||
.hasContent("\"" + (file + File.pathSeparator + file1).replace("\\", "\\\\") + "\""); |
||||
} |
||||
|
||||
} |
||||
|
||||
@Nested |
||||
@DisabledOnOs(OS.WINDOWS) |
||||
class UnixTests { |
||||
|
||||
@Test |
||||
void buildWithEmptyClassPath() { |
||||
Classpath classpath = ClasspathBuilder.forURLs().build(); |
||||
assertThat(classpath.argument()).isEmpty(); |
||||
assertThat(classpath.elements()).isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void buildWithSingleClassPathURL(@TempDir Path tempDir) throws Exception { |
||||
Path file = tempDir.resolve("test.jar"); |
||||
Classpath classpath = ClasspathBuilder.forURLs(file.toUri().toURL()).build(); |
||||
assertThat(classpath.argument()).isEqualTo(file.toString()); |
||||
assertThat(classpath.elements()).singleElement().isEqualTo(file); |
||||
} |
||||
|
||||
@Test |
||||
void buildWithMultipleClassPathURLs(@TempDir Path tempDir) throws Exception { |
||||
Path file = tempDir.resolve("test.jar"); |
||||
Path file1 = tempDir.resolve("test1.jar"); |
||||
assertThat(ClasspathBuilder.forURLs(file.toUri().toURL(), file1.toUri().toURL()).build().argument()) |
||||
.isEqualTo(file + File.pathSeparator + file1); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue