Support flat jar layering with Gradle
Update the Gralde plugin so that layered jars now use the regular "flat" format. The layers.idx file now describes which layer each file should be placed. See gh-20813 Co-authored-by: Phillip Webb <pwebb@pivotal.io>
This commit is contained in:
committed by
Phillip Webb
parent
4e3cdf936f
commit
bfa04e6574
@@ -437,12 +437,16 @@ abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> {
|
||||
}
|
||||
|
||||
protected List<String> getEntryNames(File file) throws IOException {
|
||||
List<String> entryNames = new ArrayList<>();
|
||||
try (JarFile jarFile = new JarFile(file)) {
|
||||
Enumeration<JarEntry> entries = jarFile.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
entryNames.add(entries.nextElement().getName());
|
||||
}
|
||||
return getEntryNames(jarFile);
|
||||
}
|
||||
}
|
||||
|
||||
protected List<String> getEntryNames(JarFile jarFile) {
|
||||
List<String> entryNames = new ArrayList<>();
|
||||
Enumeration<JarEntry> entries = jarFile.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
entryNames.add(entries.nextElement().getName());
|
||||
}
|
||||
return entryNames;
|
||||
}
|
||||
|
||||
@@ -16,14 +16,25 @@
|
||||
|
||||
package org.springframework.boot.gradle.tasks.bundling;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringReader;
|
||||
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.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
import org.gradle.testkit.runner.BuildResult;
|
||||
import org.gradle.testkit.runner.InvalidRunnerConfigurationException;
|
||||
@@ -77,14 +88,45 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests {
|
||||
writeMainClass();
|
||||
writeResource();
|
||||
assertThat(this.gradleBuild.build("bootJar").task(":bootJar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
|
||||
Map<String, List<String>> indexedLayers;
|
||||
String layerToolsJar = "BOOT-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getName();
|
||||
try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) {
|
||||
assertThat(jarFile.getEntry(jarModeLayerTools())).isNotNull();
|
||||
assertThat(jarFile.getEntry("BOOT-INF/layers/dependencies/lib/commons-lang3-3.9.jar")).isNotNull();
|
||||
assertThat(jarFile.getEntry("BOOT-INF/layers/snapshot-dependencies/lib/commons-io-2.7-SNAPSHOT.jar"))
|
||||
.isNotNull();
|
||||
assertThat(jarFile.getEntry("BOOT-INF/layers/application/classes/example/Main.class")).isNotNull();
|
||||
assertThat(jarFile.getEntry("BOOT-INF/layers/application/classes/static/file.txt")).isNotNull();
|
||||
assertThat(jarFile.getEntry(layerToolsJar)).isNotNull();
|
||||
assertThat(jarFile.getEntry("BOOT-INF/lib/commons-lang3-3.9.jar")).isNotNull();
|
||||
assertThat(jarFile.getEntry("BOOT-INF/lib/spring-core-5.2.5.RELEASE.jar")).isNotNull();
|
||||
assertThat(jarFile.getEntry("BOOT-INF/lib/spring-jcl-5.2.5.RELEASE.jar")).isNotNull();
|
||||
assertThat(jarFile.getEntry("BOOT-INF/lib/commons-io-2.7-SNAPSHOT.jar")).isNotNull();
|
||||
assertThat(jarFile.getEntry("BOOT-INF/classes/example/Main.class")).isNotNull();
|
||||
assertThat(jarFile.getEntry("BOOT-INF/classes/static/file.txt")).isNotNull();
|
||||
indexedLayers = readLayerIndex(jarFile);
|
||||
}
|
||||
List<String> layerNames = Arrays.asList("dependencies", "spring-boot-loader", "snapshot-dependencies",
|
||||
"application");
|
||||
assertThat(indexedLayers.keySet()).containsExactlyElementsOf(layerNames);
|
||||
List<String> expectedDependencies = new ArrayList<>();
|
||||
expectedDependencies.add("BOOT-INF/lib/commons-lang3-3.9.jar");
|
||||
expectedDependencies.add("BOOT-INF/lib/spring-core-5.2.5.RELEASE.jar");
|
||||
expectedDependencies.add("BOOT-INF/lib/spring-jcl-5.2.5.RELEASE.jar");
|
||||
List<String> expectedSnapshotDependencies = new ArrayList<>();
|
||||
expectedSnapshotDependencies.add("BOOT-INF/lib/commons-io-2.7-SNAPSHOT.jar");
|
||||
(layerToolsJar.contains("SNAPSHOT") ? expectedSnapshotDependencies : expectedDependencies).add(layerToolsJar);
|
||||
assertThat(indexedLayers.get("dependencies")).containsExactlyElementsOf(expectedDependencies);
|
||||
assertThat(indexedLayers.get("spring-boot-loader"))
|
||||
.allMatch(Pattern.compile("org/springframework/boot/loader/.+\\.class").asPredicate());
|
||||
assertThat(indexedLayers.get("snapshot-dependencies")).containsExactlyElementsOf(expectedSnapshotDependencies);
|
||||
assertThat(indexedLayers.get("application")).containsExactly("META-INF/MANIFEST.MF",
|
||||
"BOOT-INF/classes/example/Main.class", "BOOT-INF/classes/static/file.txt", "BOOT-INF/classpath.idx",
|
||||
"BOOT-INF/layers.idx");
|
||||
BuildResult listLayers = this.gradleBuild.build("listLayers");
|
||||
assertThat(listLayers.task(":listLayers").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
|
||||
String listLayersOutput = listLayers.getOutput();
|
||||
assertThat(new BufferedReader(new StringReader(listLayersOutput)).lines()).containsSequence(layerNames);
|
||||
BuildResult extractLayers = this.gradleBuild.build("extractLayers");
|
||||
assertThat(extractLayers.task(":extractLayers").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
|
||||
Map<String, List<String>> extractedLayers = readExtractedLayers(this.gradleBuild.getProjectDir(), layerNames);
|
||||
assertThat(extractedLayers.keySet()).isEqualTo(indexedLayers.keySet());
|
||||
extractedLayers.forEach(
|
||||
(name, contents) -> assertThat(contents).containsExactlyInAnyOrderElementsOf(indexedLayers.get(name)));
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
@@ -92,23 +134,49 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests {
|
||||
writeMainClass();
|
||||
writeResource();
|
||||
BuildResult build = this.gradleBuild.build("bootJar");
|
||||
System.out.println(build.getOutput());
|
||||
assertThat(build.task(":bootJar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
|
||||
Map<String, List<String>> indexedLayers;
|
||||
String layerToolsJar = "BOOT-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getName();
|
||||
try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) {
|
||||
assertThat(jarFile.getEntry(jarModeLayerTools())).isNotNull();
|
||||
assertThat(jarFile.getEntry("BOOT-INF/layers/commons-dependencies/lib/commons-lang3-3.9.jar")).isNotNull();
|
||||
assertThat(jarFile.getEntry("BOOT-INF/layers/snapshot-dependencies/lib/commons-io-2.7-SNAPSHOT.jar"))
|
||||
.isNotNull();
|
||||
assertThat(jarFile.getEntry("BOOT-INF/layers/app/classes/example/Main.class")).isNotNull();
|
||||
assertThat(jarFile.getEntry("BOOT-INF/layers/static/classes/static/file.txt")).isNotNull();
|
||||
assertThat(jarFile.getEntry(layerToolsJar)).isNotNull();
|
||||
assertThat(jarFile.getEntry("BOOT-INF/lib/commons-lang3-3.9.jar")).isNotNull();
|
||||
assertThat(jarFile.getEntry("BOOT-INF/lib/spring-core-5.2.5.RELEASE.jar")).isNotNull();
|
||||
assertThat(jarFile.getEntry("BOOT-INF/lib/spring-jcl-5.2.5.RELEASE.jar")).isNotNull();
|
||||
assertThat(jarFile.getEntry("BOOT-INF/lib/commons-io-2.7-SNAPSHOT.jar")).isNotNull();
|
||||
assertThat(jarFile.getEntry("BOOT-INF/classes/example/Main.class")).isNotNull();
|
||||
assertThat(jarFile.getEntry("BOOT-INF/classes/static/file.txt")).isNotNull();
|
||||
assertThat(jarFile.getEntry("BOOT-INF/layers.idx")).isNotNull();
|
||||
indexedLayers = readLayerIndex(jarFile);
|
||||
}
|
||||
}
|
||||
|
||||
private String jarModeLayerTools() {
|
||||
JarModeLibrary library = JarModeLibrary.LAYER_TOOLS;
|
||||
String version = library.getCoordinates().getVersion();
|
||||
String layer = (version == null || !version.contains("SNAPSHOT")) ? "dependencies" : "snapshot-dependencies";
|
||||
return "BOOT-INF/layers/" + layer + "/lib/" + library.getName();
|
||||
List<String> layerNames = Arrays.asList("dependencies", "commons-dependencies", "snapshot-dependencies",
|
||||
"static", "app");
|
||||
assertThat(indexedLayers.keySet()).containsExactlyElementsOf(layerNames);
|
||||
List<String> expectedDependencies = new ArrayList<>();
|
||||
expectedDependencies.add("BOOT-INF/lib/spring-core-5.2.5.RELEASE.jar");
|
||||
expectedDependencies.add("BOOT-INF/lib/spring-jcl-5.2.5.RELEASE.jar");
|
||||
List<String> expectedSnapshotDependencies = new ArrayList<>();
|
||||
expectedSnapshotDependencies.add("BOOT-INF/lib/commons-io-2.7-SNAPSHOT.jar");
|
||||
(layerToolsJar.contains("SNAPSHOT") ? expectedSnapshotDependencies : expectedDependencies).add(layerToolsJar);
|
||||
assertThat(indexedLayers.get("dependencies")).containsExactlyElementsOf(expectedDependencies);
|
||||
assertThat(indexedLayers.get("commons-dependencies")).containsExactly("BOOT-INF/lib/commons-lang3-3.9.jar");
|
||||
assertThat(indexedLayers.get("snapshot-dependencies")).containsExactlyElementsOf(expectedSnapshotDependencies);
|
||||
assertThat(indexedLayers.get("static")).containsExactly("BOOT-INF/classes/static/file.txt");
|
||||
List<String> appLayer = new ArrayList<>(indexedLayers.get("app"));
|
||||
List<String> nonLoaderEntries = Arrays.asList("META-INF/MANIFEST.MF", "BOOT-INF/classes/example/Main.class",
|
||||
"BOOT-INF/classpath.idx", "BOOT-INF/layers.idx");
|
||||
assertThat(appLayer).containsSubsequence(nonLoaderEntries);
|
||||
appLayer.removeAll(nonLoaderEntries);
|
||||
assertThat(appLayer).allMatch(Pattern.compile("org/springframework/boot/loader/.+\\.class").asPredicate());
|
||||
BuildResult listLayers = this.gradleBuild.build("listLayers");
|
||||
assertThat(listLayers.task(":listLayers").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
|
||||
String listLayersOutput = listLayers.getOutput();
|
||||
assertThat(new BufferedReader(new StringReader(listLayersOutput)).lines()).containsSequence(layerNames);
|
||||
BuildResult extractLayers = this.gradleBuild.build("extractLayers");
|
||||
assertThat(extractLayers.task(":extractLayers").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
|
||||
Map<String, List<String>> extractedLayers = readExtractedLayers(this.gradleBuild.getProjectDir(), layerNames);
|
||||
assertThat(extractedLayers.keySet()).isEqualTo(indexedLayers.keySet());
|
||||
extractedLayers.forEach(
|
||||
(name, contents) -> assertThat(contents).containsExactlyInAnyOrderElementsOf(indexedLayers.get(name)));
|
||||
}
|
||||
|
||||
private void writeMainClass() {
|
||||
@@ -144,4 +212,24 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests {
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, List<String>> readLayerIndex(JarFile jarFile) throws IOException {
|
||||
ZipEntry indexEntry = jarFile.getEntry("BOOT-INF/layers.idx");
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(jarFile.getInputStream(indexEntry)))) {
|
||||
return reader.lines().map((line) -> line.split(" "))
|
||||
.collect(Collectors.groupingBy((layerAndPath) -> layerAndPath[0], LinkedHashMap::new,
|
||||
Collectors.mapping((layerAndPath) -> layerAndPath[1], Collectors.toList())));
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, List<String>> readExtractedLayers(File root, List<String> layerNames) throws IOException {
|
||||
Map<String, List<String>> extractedLayers = new LinkedHashMap<>();
|
||||
for (String layerName : layerNames) {
|
||||
File layer = new File(root, layerName);
|
||||
assertThat(layer).isDirectory();
|
||||
extractedLayers.put(layerName, Files.walk(layer.toPath()).filter((path) -> path.toFile().isFile())
|
||||
.map(layer.toPath()::relativize).map(Path::toString).collect(Collectors.toList()));
|
||||
}
|
||||
return extractedLayers;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -79,46 +79,41 @@ class BootJarTests extends AbstractBootArchiveTests<TestBootJar> {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenJarIsLayeredThenBootInfContainsOnlyLayersAndIndexFiles() throws IOException {
|
||||
List<String> entryNames = getEntryNames(createLayeredJar());
|
||||
assertThat(entryNames.stream().filter((name) -> name.startsWith("BOOT-INF/"))
|
||||
.filter((name) -> !name.startsWith("BOOT-INF/layers/"))).contains("BOOT-INF/layers.idx",
|
||||
"BOOT-INF/classpath.idx");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenJarIsLayeredThenManifestContainsEntryForLayersIndexInPlaceOfClassesAndLib() throws IOException {
|
||||
try (JarFile jarFile = new JarFile(createLayeredJar())) {
|
||||
assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Classes")).isEqualTo(null);
|
||||
assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Lib")).isEqualTo(null);
|
||||
assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Classes"))
|
||||
.isEqualTo("BOOT-INF/classes/");
|
||||
assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Lib"))
|
||||
.isEqualTo("BOOT-INF/lib/");
|
||||
assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Classpath-Index"))
|
||||
.isEqualTo("BOOT-INF/classpath.idx");
|
||||
assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Layers-Index"))
|
||||
.isEqualTo("BOOT-INF/layers.idx");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenJarIsLayeredThenLayersIndexIsPresentAndListsLayersInOrder() throws IOException {
|
||||
void whenJarIsLayeredThenLayersIndexIsPresentAndCorrect() throws IOException {
|
||||
try (JarFile jarFile = new JarFile(createLayeredJar())) {
|
||||
assertThat(entryLines(jarFile, "BOOT-INF/layers.idx")).containsExactly("dependencies", "spring-boot-loader",
|
||||
List<String> entryNames = getEntryNames(jarFile);
|
||||
assertThat(entryNames).contains("BOOT-INF/lib/first-library.jar", "BOOT-INF/lib/second-library.jar",
|
||||
"BOOT-INF/lib/third-library-SNAPSHOT.jar", "BOOT-INF/classes/com/example/Application.class",
|
||||
"BOOT-INF/classes/application.properties", "BOOT-INF/classes/static/test.css");
|
||||
List<String> index = entryLines(jarFile, "BOOT-INF/layers.idx");
|
||||
assertThat(getLayerNames(index)).containsExactly("dependencies", "spring-boot-loader",
|
||||
"snapshot-dependencies", "application");
|
||||
assertThat(index).contains("dependencies BOOT-INF/lib/first-library.jar",
|
||||
"dependencies BOOT-INF/lib/second-library.jar",
|
||||
"snapshot-dependencies BOOT-INF/lib/third-library-SNAPSHOT.jar",
|
||||
"application BOOT-INF/classes/com/example/Application.class",
|
||||
"application BOOT-INF/classes/application.properties",
|
||||
"application BOOT-INF/classes/static/test.css");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenJarIsLayeredThenContentsAreMovedToLayerDirectories() throws IOException {
|
||||
List<String> entryNames = getEntryNames(createLayeredJar());
|
||||
assertThat(entryNames)
|
||||
.containsSubsequence("BOOT-INF/layers/dependencies/lib/first-library.jar",
|
||||
"BOOT-INF/layers/dependencies/lib/second-library.jar")
|
||||
.contains("BOOT-INF/layers/snapshot-dependencies/lib/third-library-SNAPSHOT.jar")
|
||||
.containsSubsequence("BOOT-INF/layers/application/classes/com/example/Application.class",
|
||||
"BOOT-INF/layers/application/classes/application.properties")
|
||||
.contains("BOOT-INF/layers/application/classes/static/test.css");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenJarIsLayeredWithCustomStrategiesThenContentsAreMovedToLayerDirectories() throws IOException {
|
||||
void whenJarIsLayeredWithCustomStrategiesThenLayersIndexIsPresentAndCorrent() throws IOException {
|
||||
File jar = createLayeredJar((layered) -> {
|
||||
layered.application((application) -> {
|
||||
application.intoLayer("resources", (spec) -> spec.include("static/**"));
|
||||
@@ -131,25 +126,30 @@ class BootJarTests extends AbstractBootArchiveTests<TestBootJar> {
|
||||
});
|
||||
layered.layerOrder("my-deps", "my-internal-deps", "my-snapshot-deps", "resources", "application");
|
||||
});
|
||||
List<String> entryNames = getEntryNames(jar);
|
||||
assertThat(entryNames)
|
||||
.containsSubsequence("BOOT-INF/layers/my-internal-deps/lib/first-library.jar",
|
||||
"BOOT-INF/layers/my-internal-deps/lib/second-library.jar")
|
||||
.contains("BOOT-INF/layers/my-snapshot-deps/lib/third-library-SNAPSHOT.jar")
|
||||
.containsSubsequence("BOOT-INF/layers/application/classes/com/example/Application.class",
|
||||
"BOOT-INF/layers/application/classes/application.properties")
|
||||
.contains("BOOT-INF/layers/resources/classes/static/test.css");
|
||||
try (JarFile jarFile = new JarFile(jar)) {
|
||||
List<String> entryNames = getEntryNames(jar);
|
||||
assertThat(entryNames).contains("BOOT-INF/lib/first-library.jar", "BOOT-INF/lib/second-library.jar",
|
||||
"BOOT-INF/lib/third-library-SNAPSHOT.jar", "BOOT-INF/classes/com/example/Application.class",
|
||||
"BOOT-INF/classes/application.properties", "BOOT-INF/classes/static/test.css");
|
||||
List<String> index = entryLines(jarFile, "BOOT-INF/layers.idx");
|
||||
assertThat(getLayerNames(index)).containsExactly("my-deps", "my-internal-deps", "my-snapshot-deps",
|
||||
"resources", "application");
|
||||
assertThat(index).contains("my-internal-deps BOOT-INF/lib/first-library.jar",
|
||||
"my-internal-deps BOOT-INF/lib/second-library.jar",
|
||||
"my-snapshot-deps BOOT-INF/lib/third-library-SNAPSHOT.jar",
|
||||
"application BOOT-INF/classes/com/example/Application.class",
|
||||
"application BOOT-INF/classes/application.properties",
|
||||
"resources BOOT-INF/classes/static/test.css");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenJarIsLayeredJarsInLibAreStored() throws IOException {
|
||||
void jarsInLibAreStored() throws IOException {
|
||||
try (JarFile jarFile = new JarFile(createLayeredJar())) {
|
||||
assertThat(jarFile.getEntry("BOOT-INF/layers/dependencies/lib/first-library.jar").getMethod())
|
||||
assertThat(jarFile.getEntry("BOOT-INF/lib/first-library.jar").getMethod()).isEqualTo(ZipEntry.STORED);
|
||||
assertThat(jarFile.getEntry("BOOT-INF/lib/second-library.jar").getMethod()).isEqualTo(ZipEntry.STORED);
|
||||
assertThat(jarFile.getEntry("BOOT-INF/lib/third-library-SNAPSHOT.jar").getMethod())
|
||||
.isEqualTo(ZipEntry.STORED);
|
||||
assertThat(jarFile.getEntry("BOOT-INF/layers/dependencies/lib/second-library.jar").getMethod())
|
||||
.isEqualTo(ZipEntry.STORED);
|
||||
assertThat(jarFile.getEntry("BOOT-INF/layers/snapshot-dependencies/lib/third-library-SNAPSHOT.jar")
|
||||
.getMethod()).isEqualTo(ZipEntry.STORED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,14 +164,7 @@ class BootJarTests extends AbstractBootArchiveTests<TestBootJar> {
|
||||
@Test
|
||||
void whenJarIsLayeredThenLayerToolsAreAddedToTheJar() throws IOException {
|
||||
List<String> entryNames = getEntryNames(createLayeredJar());
|
||||
assertThat(entryNames).contains(jarModeLayerTools());
|
||||
}
|
||||
|
||||
private String jarModeLayerTools() {
|
||||
JarModeLibrary library = JarModeLibrary.LAYER_TOOLS;
|
||||
String version = library.getCoordinates().getVersion();
|
||||
String layer = (version == null || !version.contains("SNAPSHOT")) ? "dependencies" : "snapshot-dependencies";
|
||||
return "BOOT-INF/layers/" + layer + "/lib/" + library.getName();
|
||||
assertThat(entryNames).contains("BOOT-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -267,6 +260,14 @@ class BootJarTests extends AbstractBootArchiveTests<TestBootJar> {
|
||||
}
|
||||
}
|
||||
|
||||
private Set<String> getLayerNames(List<String> index) {
|
||||
return index.stream().map(this::getLayerName).collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
}
|
||||
|
||||
private String getLayerName(String indexLine) {
|
||||
return indexLine.substring(0, indexLine.indexOf(" "));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void executeTask() {
|
||||
getTask().copy();
|
||||
|
||||
Reference in New Issue
Block a user