ClassPathContentHandler can survive tmp folder cleanup (#2361)

* ClassPathContentHandler can survive tmp folder cleanup

Signed-off-by: Daniel Kec <daniel.kec@oracle.com>
This commit is contained in:
Daniel Kec
2020-09-30 21:44:59 +02:00
committed by GitHub
parent 24e39b9710
commit 86e7cdc8bd
5 changed files with 195 additions and 10 deletions

View File

@@ -212,6 +212,9 @@ public class ServerCdiExtension implements Extension {
cpBuilder.welcomeFileName(config.get("welcome")
.asString()
.orElse("index.html"));
config.get("tmp-dir")
.as(Path.class)
.ifPresent(cpBuilder::tmpDir);
StaticContentSupport staticContent = cpBuilder.build();
if (context.exists()) {

View File

@@ -49,14 +49,17 @@ class ClassPathContentHandler extends StaticContentHandler {
private final Map<String, ExtractedJarEntry> extracted = new ConcurrentHashMap<>();
private final String root;
private final String rootWithTrailingSlash;
private final Path tempDir;
ClassPathContentHandler(String welcomeFilename,
ContentTypeSelector contentTypeSelector,
String root,
Path tempDir,
ClassLoader classLoader) {
super(welcomeFilename, contentTypeSelector);
this.classLoader = (classLoader == null) ? this.getClass().getClassLoader() : classLoader;
this.tempDir = tempDir;
this.root = root;
this.rootWithTrailingSlash = root + '/';
}
@@ -64,6 +67,7 @@ class ClassPathContentHandler extends StaticContentHandler {
public static StaticContentHandler create(String welcomeFileName,
ContentTypeSelector selector,
String clRoot,
Path tempDir,
ClassLoader classLoader) {
ClassLoader contentClassloader = (classLoader == null)
? ClassPathContentHandler.class.getClassLoader()
@@ -79,7 +83,7 @@ class ClassPathContentHandler extends StaticContentHandler {
throw new IllegalArgumentException("Cannot serve full classpath, please configure a classpath prefix");
}
return new ClassPathContentHandler(welcomeFileName, selector, clRoot, contentClassloader);
return new ClassPathContentHandler(welcomeFileName, selector, clRoot, tempDir, contentClassloader);
}
@SuppressWarnings("checkstyle:RegexpSinglelineJava")
@@ -146,15 +150,16 @@ class ClassPathContentHandler extends StaticContentHandler {
return true;
}
private boolean sendJar(Http.RequestMethod method,
String requestedResource,
URL url,
ServerRequest request,
ServerResponse response) {
boolean sendJar(Http.RequestMethod method,
String requestedResource,
URL url,
ServerRequest request,
ServerResponse response) {
LOGGER.fine(() -> "Sending static content from classpath: " + url);
ExtractedJarEntry extrEntry = extracted.computeIfAbsent(requestedResource, thePath -> extractJarEntry(url));
ExtractedJarEntry extrEntry = extracted
.compute(requestedResource, (key, entry) -> existOrCreate(url, entry));
if (extrEntry.tempFile == null) {
return false;
}
@@ -179,6 +184,13 @@ class ClassPathContentHandler extends StaticContentHandler {
return true;
}
private ExtractedJarEntry existOrCreate(URL url, ExtractedJarEntry entry) {
if (entry == null) return extractJarEntry(url);
if (entry.tempFile == null) return entry;
if (Files.notExists(entry.tempFile)) return extractJarEntry(url);
return entry;
}
private void sendUrlStream(Http.RequestMethod method, URL url, ServerRequest request, ServerResponse response)
throws IOException {
@@ -228,7 +240,7 @@ class ClassPathContentHandler extends StaticContentHandler {
// Extract JAR entry to file
try (InputStream is = jarFile.getInputStream(jarEntry)) {
Path tempFile = Files.createTempFile("ws", ".je");
Path tempFile = createTempFile("ws", ".je");
Files.copy(is, tempFile, StandardCopyOption.REPLACE_EXISTING);
return new ExtractedJarEntry(tempFile, lastModified, jarEntry.getName());
} finally {
@@ -241,6 +253,22 @@ class ClassPathContentHandler extends StaticContentHandler {
}
}
/**
* Create temp file in provided temp folder, or default one.
*
* @param prefix string to be used in generating the file's name.
* @param suffix string to be used in generating the file's name.
* @return the path to the newly created file
* @throws IOException
*/
private Path createTempFile(String prefix, String suffix) throws IOException {
if (tempDir != null) {
return Files.createTempFile(tempDir, prefix, suffix);
} else {
return Files.createTempFile(prefix, suffix);
}
}
private Instant getLastModified(String path) throws IOException {
Path file = Paths.get(path);

View File

@@ -166,6 +166,7 @@ public class StaticContentSupport implements Service {
private final Map<String, MediaType> specificContentTypes = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
private String welcomeFileName;
private Path tmpDir;
Builder(Path fsRoot) {
Objects.requireNonNull(fsRoot, "Attribute fsRoot is null!");
@@ -195,6 +196,17 @@ public class StaticContentSupport implements Service {
return this;
}
/**
* Sets custom temporary folder for extracting static content from a jar.
*
* @param tmpDir custom temporary folder
* @return updated builder
*/
public Builder tmpDir(Path tmpDir) {
this.tmpDir = tmpDir;
return this;
}
/**
* Maps a filename extension to the response content type.
*
@@ -230,7 +242,7 @@ public class StaticContentSupport implements Service {
if (fsRoot != null) {
handler = FileSystemContentHandler.create(welcomeFileName, selector, fsRoot);
} else if (clRoot != null) {
handler = ClassPathContentHandler.create(welcomeFileName, selector, clRoot, classLoader);
handler = ClassPathContentHandler.create(welcomeFileName, selector, clRoot, tmpDir, classLoader);
} else {
throw new IllegalArgumentException("Builder was created without specified static content root!");
}

View File

@@ -261,7 +261,7 @@ public class StaticContentHandlerTest {
final boolean returnValue;
TestClassPathContentHandler(String welcomeFilename, ContentTypeSelector contentTypeSelector, Path root, boolean returnValue) {
super(welcomeFilename, contentTypeSelector, root.toString(), null);
super(welcomeFilename, contentTypeSelector, root.toString(), null, null);
this.returnValue = returnValue;
}

View File

@@ -0,0 +1,142 @@
/*
* Copyright (c) 2020 Oracle and/or its affiliates.
*
* 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
*
* http://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 io.helidon.webserver;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Flow;
import java.util.function.Function;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.logging.Logger;
import io.helidon.common.http.DataChunk;
import io.helidon.common.http.HashParameters;
import io.helidon.common.http.Http;
import io.helidon.common.reactive.Single;
import io.helidon.media.common.MessageBodyWriterContext;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
public class UnstableTempTest {
private static final Logger LOGGER = Logger.getLogger(UnstableTempTest.class.getName());
private static final String JAR_NAME = "test.jar";
private static final String FILE_NAME = "test.js";
private static final String FILE_CONTENT = "alert(\"Hello, World!\");";
private static Path tmpDir;
@BeforeAll
static void beforeAll() throws IOException, URISyntaxException {
tmpDir = Paths.get(UnstableTempTest.class.getResource("").toURI()).resolve("tmp");
Files.createDirectories(tmpDir);
}
@Test
void cleanedTmpDuringRuntime() throws IOException {
List<String> contents = new ArrayList<>(2);
Path jar = createJar();
URL jarUrl = new URL("jar:file:" + jar.toUri().getPath() + "!/" + FILE_NAME);
LOGGER.fine(() -> "Generated test jar url: " + jarUrl.toString());
ClassPathContentHandler classPathContentHandler =
new ClassPathContentHandler(null,
new ContentTypeSelector(null),
"/",
tmpDir,
Thread.currentThread().getContextClassLoader());
// Empty headers
RequestHeaders headers = mock(RequestHeaders.class);
when(headers.isAccepted(any())).thenReturn(true);
when(headers.acceptedTypes()).thenReturn(Collections.emptyList());
ResponseHeaders responseHeaders = mock(ResponseHeaders.class);
ServerRequest request = Mockito.mock(ServerRequest.class);
Mockito.when(request.headers()).thenReturn(headers);
ServerResponse response = Mockito.mock(ServerResponse.class);
MessageBodyWriterContext ctx = MessageBodyWriterContext.create(HashParameters.create());
ctx.registerFilter(dataChunkPub -> {
String fileContent = new String(Single.create(dataChunkPub).await().bytes());
contents.add(fileContent);
return Single.just(DataChunk.create(ByteBuffer.wrap(fileContent.getBytes())));
});
Mockito.when(response.headers()).thenReturn(responseHeaders);
Mockito.when(response.send(Mockito.any(Function.class))).then(mock -> {
Function<MessageBodyWriterContext, Flow.Publisher<DataChunk>> argument = mock.getArgument(0);
return Single.create(argument.apply(ctx)).onError(throwable -> throwable.printStackTrace());
});
classPathContentHandler.sendJar(Http.Method.GET, FILE_NAME, jarUrl, request, response);
deleteTmpFiles();
classPathContentHandler.sendJar(Http.Method.GET, FILE_NAME, jarUrl, request, response);
assertThat(contents, containsInAnyOrder(FILE_CONTENT, FILE_CONTENT));
}
private void deleteTmpFiles() throws IOException {
LOGGER.fine(() -> "Cleaning temp dir: " + tmpDir);
Files.list(tmpDir)
.forEach(path -> {
try {
Files.deleteIfExists(path);
} catch (IOException e) {
fail("Unable to delete " + path.getFileName(), e);
}
});
}
private Path createJar() {
try {
Path testJar = Path.of(UnstableTempTest.class.getResource("").toURI()).resolve(JAR_NAME);
Manifest manifest = new Manifest();
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
JarOutputStream target = new JarOutputStream(new FileOutputStream(testJar.toFile()), manifest);
JarEntry entry = new JarEntry(FILE_NAME);
BufferedOutputStream bos = new BufferedOutputStream(target);
target.putNextEntry(entry);
bos.write(FILE_CONTENT.getBytes());
bos.close();
target.close();
return testJar;
} catch (IOException | URISyntaxException e) {
throw new RuntimeException(e);
}
}
}