Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,16 @@ internal class GradlePluginAdapter private constructor(
return _classFinder
}

fun closeClassFinder() {
if (::_classFinder.isInitialized && _classFinder is AutoCloseable) {
try {
(_classFinder as AutoCloseable).close()
} catch (e: Exception) {
logger.debug("Error closing ClassFinder", e)
}
}
}

private fun createClassFinderClasspath(
project: Project,
dependencyConfiguration: Configuration?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,69 +195,74 @@ public abstract class VaadinBuildFrontendTask : DefaultTask() {

@TaskAction
public fun vaadinBuildFrontend() {
val config = adapter.get().config
logger.info("Running the vaadinBuildFrontend task with effective configuration $config")
// Propagate build info to the token file so that runNodeUpdater
// and the frontend build have the configuration they need.
BuildFrontendUtil.propagateBuildInfo(adapter.get())
try {
val config = adapter.get().config
logger.info("Running the vaadinBuildFrontend task with effective configuration $config")
// Propagate build info to the token file so that runNodeUpdater
// and the frontend build have the configuration they need.
BuildFrontendUtil.propagateBuildInfo(adapter.get())

val options = Options(null, adapter.get().classFinder, config.npmFolder.get())
.withFrontendDirectory(BuildFrontendUtil.getFrontendDirectory(adapter.get()))
.withFrontendGeneratedFolder(config.generatedTsFolder.get())
val cleanTask = TaskCleanFrontendFiles(options)

val reactEnabled: Boolean = adapter.get().isReactEnabled()
&& FrontendUtils.isReactRouterRequired(
BuildFrontendUtil.getFrontendDirectory(adapter.get())
)
val featureFlags: FeatureFlags = FeatureFlags(
adapter.get().createLookup(adapter.get().getClassFinder())
)
if (adapter.get().javaResourceFolder() != null) {
featureFlags.setPropertiesLocation(adapter.get().javaResourceFolder())
}
val frontendDependencies: FrontendDependenciesScanner = FrontendDependenciesScannerFactory()
.createScanner(
!adapter.get().optimizeBundle(), adapter.get().getClassFinder(),
adapter.get().generateEmbeddableWebComponents(), featureFlags,
reactEnabled
val options = Options(null, adapter.get().classFinder, config.npmFolder.get())
.withFrontendDirectory(BuildFrontendUtil.getFrontendDirectory(adapter.get()))
.withFrontendGeneratedFolder(config.generatedTsFolder.get())
val cleanTask = TaskCleanFrontendFiles(options)

val reactEnabled: Boolean = adapter.get().isReactEnabled()
&& FrontendUtils.isReactRouterRequired(
BuildFrontendUtil.getFrontendDirectory(adapter.get())
)
val featureFlags: FeatureFlags = FeatureFlags(
adapter.get().createLookup(adapter.get().getClassFinder())
)
if (adapter.get().javaResourceFolder() != null) {
featureFlags.setPropertiesLocation(adapter.get().javaResourceFolder())
}
val frontendDependencies: FrontendDependenciesScanner = FrontendDependenciesScannerFactory()
.createScanner(
!adapter.get().optimizeBundle(), adapter.get().getClassFinder(),
adapter.get().generateEmbeddableWebComponents(), featureFlags,
reactEnabled
)

BuildFrontendUtil.runNodeUpdater(adapter.get(), frontendDependencies)
BuildFrontendUtil.runNodeUpdater(adapter.get(), frontendDependencies)

if (adapter.get().generateBundle() && BundleValidationUtil.needsBundleBuild
(adapter.get().servletResourceOutputDirectory())) {
BuildFrontendUtil.runFrontendBuild(adapter.get())
if (cleanFrontendFiles()) {
cleanTask.execute()
if (adapter.get().generateBundle() && BundleValidationUtil.needsBundleBuild
(adapter.get().servletResourceOutputDirectory())) {
BuildFrontendUtil.runFrontendBuild(adapter.get())
if (cleanFrontendFiles()) {
cleanTask.execute()
}
}
LicenseChecker.setStrictOffline(true)
val (licenseRequired: Boolean, commercialBannerRequired: Boolean) = try {
Pair(
BuildFrontendUtil.validateLicenses(
adapter.get(),
frontendDependencies
), false
)
} catch (e: MissingLicenseKeyException) {
logger.info(e.message)
Pair(true, true)
}
}
LicenseChecker.setStrictOffline(true)
val (licenseRequired: Boolean, commercialBannerRequired: Boolean) = try {
Pair(
BuildFrontendUtil.validateLicenses(
adapter.get(),
frontendDependencies
), false
)
} catch (e: MissingLicenseKeyException) {
logger.info(e.message)
Pair(true, true)
}

BuildFrontendUtil.updateBuildFile(adapter.get(), licenseRequired, commercialBannerRequired
)
BuildFrontendUtil.updateBuildFile(adapter.get(), licenseRequired, commercialBannerRequired
)

// Cache the production token file and delete the original so
// that IDE runs default to development mode. Jar/War tasks
// restore the token from the cached copy in their doFirst
// action (via BuildFrontendTokenService.ensureToken()).
val tokenFile = BuildFrontendUtil.getTokenFile(adapter.get())
val cachedTokenFile = outputProperties.get().getCachedBuildInfoFile()
cachedTokenFile.parentFile.mkdirs()
if (tokenFile.exists()) {
tokenFile.copyTo(cachedTokenFile, overwrite = true)
tokenFile.delete()
// Cache the production token file and delete the original so
// that IDE runs default to development mode. Jar/War tasks
// restore the token from the cached copy in their doFirst
// action (via BuildFrontendTokenService.ensureToken()).
val tokenFile = BuildFrontendUtil.getTokenFile(adapter.get())
val cachedTokenFile = outputProperties.get().getCachedBuildInfoFile()
cachedTokenFile.parentFile.mkdirs()
if (tokenFile.exists()) {
tokenFile.copyTo(cachedTokenFile, overwrite = true)
tokenFile.delete()
}
} finally {
adapter.get().closeClassFinder()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,16 @@ public abstract class VaadinPrepareFrontendTask : DefaultTask() {

@TaskAction
public fun vaadinPrepareFrontend() {
//val adapter = GradlePluginAdapter(this, config, true)
// Remove Frontend/generated folder to get clean files copied/generated
logger.debug("Running the vaadinPrepareFrontend task with effective configuration ${adapter.get().config}")
val tokenFile = BuildFrontendUtil.propagateBuildInfo(adapter.get())
try {
// Remove Frontend/generated folder to get clean files copied/generated
logger.debug("Running the vaadinPrepareFrontend task with effective configuration ${adapter.get().config}")
val tokenFile = BuildFrontendUtil.propagateBuildInfo(adapter.get())

logger.info("Generated token file $tokenFile")
check(tokenFile.exists()) { "token file $tokenFile doesn't exist!" }
BuildFrontendUtil.prepareFrontend(adapter.get())
logger.info("Generated token file $tokenFile")
check(tokenFile.exists()) { "token file $tokenFile doesn't exist!" }
BuildFrontendUtil.prepareFrontend(adapter.get())
} finally {
adapter.get().closeClassFinder()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,15 @@
*/
package com.vaadin.flow.server.scanner;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.annotation.Repeatable;
import java.lang.reflect.AnnotatedElement;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
Expand Down Expand Up @@ -52,7 +56,7 @@
*
* @since 2.0
*/
public class ReflectionsClassFinder implements ClassFinder {
public class ReflectionsClassFinder implements ClassFinder, AutoCloseable {
/**
* System property name to be used to disable default package filtering
* during class scan. See {@link #applyScannerPackageFilters(ClassGraph)}
Expand Down Expand Up @@ -220,7 +224,28 @@ private Set<Class<?>> getAnnotatedByRepeatedAnnotation(

@Override
public URL getResource(String name) {
return classLoader.getResource(name);
URL url = classLoader.getResource(name);
if (url == null || !"jar".equals(url.getProtocol())) {
return url;
}
// Wrap jar: URLs with a handler that disables JVM-level JAR caching.
// Without this, JarFileFactory keeps a static cache of JarFile
// instances that become stale when JARs are rewritten between Gradle
// daemon builds, causing ZipException ("invalid LOC header").
try {
return new URL(null, url.toExternalForm(), new URLStreamHandler() {
@Override
protected URLConnection openConnection(URL u)
throws IOException {
URLConnection conn = new URL(u.toExternalForm())
.openConnection();
conn.setUseCaches(false);
return conn;
}
});
} catch (MalformedURLException e) {
return url;
}
}

@Override
Expand Down Expand Up @@ -272,6 +297,14 @@ public ClassLoader getClassLoader() {
return classLoader;
}

@Override
public void close() throws IOException {
if (classLoader instanceof URLClassLoader) {
LOGGER.debug("Closing URLClassLoader to release file handles");
((URLClassLoader) classLoader).close();
}
}

private <T> Set<Class<? extends T>> sortedByClassName(
Set<Class<? extends T>> source) {
return source.stream().sorted(Comparator.comparing(Class::getName))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,20 @@
import javax.tools.ToolProvider;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.stream.Collectors;

import org.junit.jupiter.api.BeforeEach;
Expand Down Expand Up @@ -247,6 +252,77 @@ private URL createTestModule(String moduleName, String pkg,
return buildDir.toURI().toURL();
}

// See https://github.com/vaadin/flow/issues/15458
@Test
void getResource_jarUrlDisablesCaching() throws Exception {
String pkg = "com.vaadin.flow.test.jar";
String className = "TestComponent";

File jarFile = Files
.createFile(externalModules.resolve("test-component.jar"))
.toFile();
createTestJar(jarFile, "jar-v1", pkg, className, "1.0.0");

try (ReflectionsClassFinder finder = new ReflectionsClassFinder(
jarFile.toURI().toURL())) {
URL resource = finder.getResource(
pkg.replace('.', '/') + "/" + className + ".class");
assertNotNull(resource, "Resource should be found in JAR");
assertEquals("jar", resource.getProtocol());

URLConnection conn = resource.openConnection();
assertFalse(conn.getUseCaches(),
"jar: URL connections should have caching disabled "
+ "to prevent stale JarFileFactory entries "
+ "under Gradle daemon");
// Verify the resource is still readable
try (InputStream is = conn.getInputStream()) {
assertTrue(is.read() != -1,
"Should be able to read class bytes");
}
}
}

private void createTestJar(File jarFile, String moduleName, String pkg,
String className, String npmPackageVersion) throws IOException {
// Compile the class to a temp directory
File sources = Files
.createDirectories(externalModules.resolve(moduleName + "/src"))
.toFile();
File sourcePkg = Files
.createDirectories(externalModules
.resolve(moduleName + "/src/" + pkg.replace('.', '/')))
.toFile();
File buildDir = Files
.createDirectories(
externalModules.resolve(moduleName + "/target"))
.toFile();

Path sourceFile = sourcePkg.toPath().resolve(className + ".java");
Files.writeString(sourceFile, String.format(CLASS_TEMPLATE, pkg,
npmPackageVersion, className), StandardCharsets.UTF_8);
compile(sourceFile.toFile(), sources, buildDir);

// Package compiled classes into a JAR
try (JarOutputStream jos = new JarOutputStream(
new FileOutputStream(jarFile))) {
Path classesRoot = buildDir.toPath();
try (var walker = Files.walk(classesRoot)) {
walker.filter(Files::isRegularFile).forEach(classFile -> {
String entryName = classesRoot.relativize(classFile)
.toString().replace(File.separatorChar, '/');
try {
jos.putNextEntry(new JarEntry(entryName));
jos.write(Files.readAllBytes(classFile));
jos.closeEntry();
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
}
}

private void compile(File sourceFile, File sourcePath, File outputPath) {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
int result = compiler.run(null, null, null, "-d", outputPath.getPath(),
Expand Down
Loading