mirror of
https://github.com/jlengrand/helidon.git
synced 2026-03-10 08:21:17 +00:00
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:
@@ -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()) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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!");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user