All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.vaadin.base.devserver.startup.DevModeInitializer Maven / Gradle / Ivy

/**
 * Copyright (C) 2000-2023 Vaadin Ltd
 *
 * This program is available under Vaadin Commercial License and Service Terms.
 *
 * See  for the full
 * license.
 */
package com.vaadin.base.devserver.startup;

import static com.vaadin.flow.server.Constants.CONNECT_APPLICATION_PROPERTIES_TOKEN;
import static com.vaadin.flow.server.Constants.CONNECT_JAVA_SOURCE_FOLDER_TOKEN;
import static com.vaadin.flow.server.Constants.CONNECT_OPEN_API_FILE_TOKEN;
import static com.vaadin.flow.server.Constants.PACKAGE_JSON;
import static com.vaadin.flow.server.Constants.PROJECT_FRONTEND_GENERATED_DIR_TOKEN;
import static com.vaadin.flow.server.Constants.VAADIN_SERVLET_RESOURCES;
import static com.vaadin.flow.server.Constants.VAADIN_WEBAPP_RESOURCES;
import static com.vaadin.flow.server.InitParameters.SERVLET_PARAMETER_DEVMODE_OPTIMIZE_BUNDLE;
import static com.vaadin.flow.server.frontend.FrontendUtils.DEFAULT_CONNECT_APPLICATION_PROPERTIES;
import static com.vaadin.flow.server.frontend.FrontendUtils.DEFAULT_CONNECT_JAVA_SOURCE_FOLDER;
import static com.vaadin.flow.server.frontend.FrontendUtils.DEFAULT_CONNECT_OPENAPI_JSON_FILE;
import static com.vaadin.flow.server.frontend.FrontendUtils.DEFAULT_FRONTEND_DIR;
import static com.vaadin.flow.server.frontend.FrontendUtils.DEFAULT_GENERATED_DIR;
import static com.vaadin.flow.server.frontend.FrontendUtils.DEFAULT_PROJECT_FRONTEND_GENERATED_DIR;
import static com.vaadin.flow.server.frontend.FrontendUtils.PARAM_FRONTEND_DIR;
import static com.vaadin.flow.server.frontend.FrontendUtils.PARAM_GENERATED_DIR;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.io.UncheckedIOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import javax.servlet.annotation.HandlesTypes;

import com.vaadin.base.devserver.ViteHandler;
import com.vaadin.base.devserver.WebpackHandler;
import com.vaadin.base.devserver.stats.DevModeUsageStatistics;
import com.vaadin.base.devserver.stats.StatisticsSender;
import com.vaadin.base.devserver.stats.StatisticsStorage;
import com.vaadin.experimental.FeatureFlags;
import com.vaadin.flow.di.Lookup;
import com.vaadin.flow.internal.DevModeHandler;
import com.vaadin.flow.server.Constants;
import com.vaadin.flow.server.ExecutionFailedException;
import com.vaadin.flow.server.InitParameters;
import com.vaadin.flow.server.VaadinContext;
import com.vaadin.flow.server.frontend.EndpointGeneratorTaskFactory;
import com.vaadin.flow.server.frontend.FallbackChunk;
import com.vaadin.flow.server.frontend.FrontendUtils;
import com.vaadin.flow.server.frontend.NodeTasks;
import com.vaadin.flow.server.frontend.NodeTasks.Builder;
import com.vaadin.flow.server.frontend.scanner.ClassFinder;
import com.vaadin.flow.server.frontend.scanner.ClassFinder.DefaultClassFinder;
import com.vaadin.flow.server.startup.ApplicationConfiguration;
import com.vaadin.flow.server.startup.VaadinInitializerException;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import elemental.json.Json;
import elemental.json.JsonObject;

/**
 * Initializer for starting node updaters as well as the dev mode server.
 * 

* For internal use only. May be renamed or removed in a future release. * * @since 2.0 */ public class DevModeInitializer implements Serializable { static class DevModeClassFinder extends DefaultClassFinder { private static final Set APPLICABLE_CLASS_NAMES = Collections .unmodifiableSet(calculateApplicableClassNames()); public DevModeClassFinder(Set> classes) { super(classes); } @Override public Set> getAnnotatedClasses( Class annotation) { ensureImplementation(annotation); return super.getAnnotatedClasses(annotation); } @Override public Set> getSubTypesOf(Class type) { ensureImplementation(type); return super.getSubTypesOf(type); } private void ensureImplementation(Class clazz) { if (!APPLICABLE_CLASS_NAMES.contains(clazz.getName())) { throw new IllegalArgumentException("Unexpected class name " + clazz + ". Implementation error: the class finder " + "instance is not aware of this class. " + "Fix @HandlesTypes annotation value for " + DevModeStartupListener.class.getName()); } } private static Set calculateApplicableClassNames() { HandlesTypes handlesTypes = DevModeStartupListener.class .getAnnotation(HandlesTypes.class); return Stream.of(handlesTypes.value()).map(Class::getName) .collect(Collectors.toSet()); } } private static final Pattern JAR_FILE_REGEX = Pattern .compile(".*file:(.+\\.jar).*"); // Path of jar files in a URL with zip protocol doesn't start with // "zip:" // nor "file:". It contains only the path of the file. // Weblogic uses zip protocol. private static final Pattern ZIP_PROTOCOL_JAR_FILE_REGEX = Pattern .compile("(.+\\.jar).*"); private static final Pattern VFS_FILE_REGEX = Pattern .compile("(vfs:/.+\\.jar).*"); private static final Pattern VFS_DIRECTORY_REGEX = Pattern .compile("vfs:/.+"); // allow trailing slash private static final Pattern DIR_REGEX_FRONTEND_DEFAULT = Pattern.compile( "^(?:file:0)?(.+)" + Constants.RESOURCES_FRONTEND_DEFAULT + "/?$"); // allow trailing slash private static final Pattern DIR_REGEX_RESOURCES_JAR_DEFAULT = Pattern .compile("^(?:file:0)?(.+)" + Constants.RESOURCES_THEME_JAR_DEFAULT + "/?$"); // allow trailing slash private static final Pattern DIR_REGEX_COMPATIBILITY_FRONTEND_DEFAULT = Pattern .compile("^(?:file:)?(.+)" + Constants.COMPATIBILITY_RESOURCES_FRONTEND_DEFAULT + "/?$"); /** * Initialize the devmode server if not in production mode or compatibility * mode. * * @param classes * classes to check for npm- and js modules * @param context * VaadinContext we are running in * @return the initialized dev mode handler or {@code null} if none was * created * * @throws VaadinInitializerException * if dev mode can't be initialized */ public static DevModeHandler initDevModeHandler(Set> classes, VaadinContext context) throws VaadinInitializerException { ApplicationConfiguration config = ApplicationConfiguration.get(context); if (config.isProductionMode()) { log().debug("Skipping DEV MODE because PRODUCTION MODE is set."); return null; } if (!config.enableDevServer()) { log().debug( "Skipping DEV MODE because dev server shouldn't be enabled."); return null; } // This needs to be set as there is no "current service" available in // this call FeatureFlags.get(context) .setPropertiesLocation(config.getJavaResourceFolder()); String baseDir = config.getStringProperty(FrontendUtils.PROJECT_BASEDIR, null); if (baseDir == null) { baseDir = getBaseDirectoryFallback(); } // Initialize the usage statistics if enabled if (config.isUsageStatisticsEnabled()) { StatisticsStorage storage = new StatisticsStorage(); DevModeUsageStatistics.init(baseDir, storage, new StatisticsSender(storage)); } String generatedDir = System.getProperty(PARAM_GENERATED_DIR, Paths.get(config.getBuildFolder(), DEFAULT_GENERATED_DIR) .toString()); String frontendFolder = config.getStringProperty(PARAM_FRONTEND_DIR, System.getProperty(PARAM_FRONTEND_DIR, DEFAULT_FRONTEND_DIR)); File flowResourcesFolder = new File(baseDir, config.getFlowResourcesFolder()); Lookup lookupFromContext = context.getAttribute(Lookup.class); Lookup lookupForClassFinder = Lookup.of(new DevModeClassFinder(classes), ClassFinder.class); Lookup lookup = Lookup.compose(lookupForClassFinder, lookupFromContext); Builder builder = new NodeTasks.Builder(lookup, new File(baseDir), new File(generatedDir), new File(frontendFolder), config.getBuildFolder()); log().info("Starting dev-mode updaters in {} folder.", builder.getNpmFolder()); if (!builder.getGeneratedFolder().exists()) { try { FileUtils.forceMkdir(builder.getGeneratedFolder()); } catch (IOException e) { throw new UncheckedIOException( String.format("Failed to create directory '%s'", builder.getGeneratedFolder()), e); } } File generatedPackages = new File(builder.getGeneratedFolder(), PACKAGE_JSON); // Regenerate webpack configuration, as it may be necessary to // update it // TODO: make sure target directories are aligned with build // config, // see https://github.com/vaadin/flow/issues/9082 File target = new File(baseDir, config.getBuildFolder()); builder.withWebpack( Paths.get(target.getPath(), "classes", VAADIN_WEBAPP_RESOURCES) .toFile(), Paths.get(target.getPath(), "classes", VAADIN_SERVLET_RESOURCES) .toFile(), FrontendUtils.WEBPACK_CONFIG, FrontendUtils.WEBPACK_GENERATED); builder.useV14Bootstrap(config.useV14Bootstrap()); if (!config.useV14Bootstrap() && isEndpointServiceAvailable(lookup)) { String connectJavaSourceFolder = config.getStringProperty( CONNECT_JAVA_SOURCE_FOLDER_TOKEN, Paths.get(baseDir, DEFAULT_CONNECT_JAVA_SOURCE_FOLDER) .toString()); String connectApplicationProperties = config.getStringProperty( CONNECT_APPLICATION_PROPERTIES_TOKEN, Paths.get(baseDir, DEFAULT_CONNECT_APPLICATION_PROPERTIES) .toString()); String connectOpenApiJsonFile = config .getStringProperty(CONNECT_OPEN_API_FILE_TOKEN, Paths.get(baseDir, config.getBuildFolder(), DEFAULT_CONNECT_OPENAPI_JSON_FILE) .toString()); builder.withFusionJavaSourceFolder( new File(connectJavaSourceFolder)) .withFusionApplicationProperties( new File(connectApplicationProperties)) .withFusionGeneratedOpenAPIJson( new File(connectOpenApiJsonFile)); } // If we are missing either the base or generated package json // files // generate those if (!new File(builder.getNpmFolder(), PACKAGE_JSON).exists() || !generatedPackages.exists()) { builder.createMissingPackageJson(true); } Set frontendLocations = getFrontendLocationsFromClassloader( DevModeStartupListener.class.getClassLoader()); boolean useByteCodeScanner = config.getBooleanProperty( SERVLET_PARAMETER_DEVMODE_OPTIMIZE_BUNDLE, Boolean.parseBoolean(System.getProperty( SERVLET_PARAMETER_DEVMODE_OPTIMIZE_BUNDLE, Boolean.FALSE.toString()))); boolean enablePnpm = config.isPnpmEnabled(); boolean useGlobalPnpm = config.isGlobalPnpm(); boolean useHomeNodeExec = config.getBooleanProperty( InitParameters.REQUIRE_HOME_NODE_EXECUTABLE, false); String fusionClientAPIFolder = config.getStringProperty( PROJECT_FRONTEND_GENERATED_DIR_TOKEN, Paths.get(baseDir, DEFAULT_PROJECT_FRONTEND_GENERATED_DIR) .toString()); JsonObject tokenFileData = Json.createObject(); NodeTasks tasks = builder.enablePackagesUpdate(true) .useByteCodeScanner(useByteCodeScanner) .withFlowResourcesFolder(flowResourcesFolder) .withFusionClientAPIFolder(new File(fusionClientAPIFolder)) .copyResources(frontendLocations) .copyLocalResources(new File(baseDir, Constants.LOCAL_FRONTEND_RESOURCES_PATH)) .enableImportsUpdate(true).runNpmInstall(true) .populateTokenFileData(tokenFileData) .withEmbeddableWebComponents(true).enablePnpm(enablePnpm) .useGlobalPnpm(useGlobalPnpm) .withHomeNodeExecRequired(useHomeNodeExec) .withProductionMode(config.isProductionMode()).build(); Runnable runnable = () -> runNodeTasks(context, tokenFileData, tasks); CompletableFuture nodeTasksFuture = CompletableFuture .runAsync(runnable); Lookup devServerLookup = Lookup.compose(lookup, Lookup.of(config, ApplicationConfiguration.class)); if (FeatureFlags.get(context).isEnabled(FeatureFlags.VITE)) { return new ViteHandler(devServerLookup, 0, builder.getNpmFolder(), nodeTasksFuture); } else { return new WebpackHandler(devServerLookup, 0, builder.getNpmFolder(), nodeTasksFuture); } } private static boolean isEndpointServiceAvailable(Lookup lookup) { if (lookup == null) { return false; } return lookup.lookup(EndpointGeneratorTaskFactory.class) != null; } private static Logger log() { return LoggerFactory.getLogger(DevModeStartupListener.class); } /* * Accept user.dir or cwd as a fallback only if the directory seems to be a * Maven or Gradle project. Check to avoid cluttering server directories * (see tickets #8249, #8403). */ private static String getBaseDirectoryFallback() { String baseDirCandidate = System.getProperty("user.dir", "."); Path path = Paths.get(baseDirCandidate); if (path.toFile().isDirectory() && (path.resolve("pom.xml").toFile().exists() || path.resolve("build.gradle").toFile().exists())) { return path.toString(); } else { throw new IllegalStateException(String.format( "Failed to determine project directory for dev mode. " + "Directory '%s' does not look like a Maven or " + "Gradle project. Ensure that you have run the " + "prepare-frontend Maven goal, which generates " + "'flow-build-info.json', prior to deploying your " + "application", path.toString())); } } /* * This method returns all folders of jar files having files in the * META-INF/resources/frontend and META-INF/resources/themes folder. We * don't use URLClassLoader because will fail in Java 9+ */ static Set getFrontendLocationsFromClassloader( ClassLoader classLoader) throws VaadinInitializerException { Set frontendFiles = new HashSet<>(); frontendFiles.addAll(getFrontendLocationsFromClassloader(classLoader, Constants.RESOURCES_FRONTEND_DEFAULT)); frontendFiles.addAll(getFrontendLocationsFromClassloader(classLoader, Constants.COMPATIBILITY_RESOURCES_FRONTEND_DEFAULT)); frontendFiles.addAll(getFrontendLocationsFromClassloader(classLoader, Constants.RESOURCES_THEME_JAR_DEFAULT)); return frontendFiles; } private static void runNodeTasks(VaadinContext vaadinContext, JsonObject tokenFileData, NodeTasks tasks) { try { tasks.execute(); FallbackChunk chunk = FrontendUtils .readFallbackChunk(tokenFileData); if (chunk != null) { vaadinContext.setAttribute(chunk); } } catch (ExecutionFailedException exception) { log().debug( "Could not initialize dev mode handler. One of the node tasks failed", exception); throw new CompletionException(exception); } } private static Set getFrontendLocationsFromClassloader( ClassLoader classLoader, String resourcesFolder) throws VaadinInitializerException { Set frontendFiles = new HashSet<>(); try { Enumeration en = classLoader.getResources(resourcesFolder); if (en == null) { return frontendFiles; } Set vfsJars = new HashSet<>(); while (en.hasMoreElements()) { URL url = en.nextElement(); String urlString = url.toString(); String path = URLDecoder.decode(url.getPath(), StandardCharsets.UTF_8.name()); Matcher jarMatcher = JAR_FILE_REGEX.matcher(path); Matcher zipProtocolJarMatcher = ZIP_PROTOCOL_JAR_FILE_REGEX .matcher(path); Matcher dirMatcher = DIR_REGEX_FRONTEND_DEFAULT.matcher(path); Matcher dirResourcesMatcher = DIR_REGEX_RESOURCES_JAR_DEFAULT .matcher(path); Matcher dirCompatibilityMatcher = DIR_REGEX_COMPATIBILITY_FRONTEND_DEFAULT .matcher(path); Matcher jarVfsMatcher = VFS_FILE_REGEX.matcher(urlString); Matcher dirVfsMatcher = VFS_DIRECTORY_REGEX.matcher(urlString); if (jarVfsMatcher.find()) { String vfsJar = jarVfsMatcher.group(1); if (vfsJars.add(vfsJar)) { // NOSONAR frontendFiles.add( getPhysicalFileOfJBossVfsJar(new URL(vfsJar))); } } else if (dirVfsMatcher.find()) { URL vfsDirUrl = new URL(urlString.substring(0, urlString.lastIndexOf(resourcesFolder))); frontendFiles .add(getPhysicalFileOfJBossVfsDirectory(vfsDirUrl)); } else if (jarMatcher.find()) { frontendFiles.add(new File(jarMatcher.group(1))); } else if ("zip".equalsIgnoreCase(url.getProtocol()) && zipProtocolJarMatcher.find()) { frontendFiles.add(new File(zipProtocolJarMatcher.group(1))); } else if (dirMatcher.find()) { frontendFiles.add(new File(dirMatcher.group(1))); } else if (dirResourcesMatcher.find()) { frontendFiles.add(new File(dirResourcesMatcher.group(1))); } else if (dirCompatibilityMatcher.find()) { frontendFiles .add(new File(dirCompatibilityMatcher.group(1))); } else { log().warn( "Resource {} not visited because does not meet supported formats.", url.getPath()); } } } catch (IOException e) { throw new UncheckedIOException(e); } return frontendFiles; } private static File getPhysicalFileOfJBossVfsDirectory(URL url) throws IOException, VaadinInitializerException { try { Object virtualFile = url.openConnection().getContent(); Class virtualFileClass = virtualFile.getClass(); // Reflection as we cannot afford a dependency to // WildFly or JBoss Method getChildrenRecursivelyMethod = virtualFileClass .getMethod("getChildrenRecursively"); Method getPhysicalFileMethod = virtualFileClass .getMethod("getPhysicalFile"); // By calling getPhysicalFile, we make sure that the // corresponding // physical files/directories of the root directory and // its children // are created. Later, these physical files are scanned // to collect // their resources. List virtualFiles = (List) getChildrenRecursivelyMethod .invoke(virtualFile); File rootDirectory = (File) getPhysicalFileMethod .invoke(virtualFile); for (Object child : virtualFiles) { // side effect: create real-world files getPhysicalFileMethod.invoke(child); } return rootDirectory; } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException exc) { throw new VaadinInitializerException( "Failed to invoke JBoss VFS API.", exc); } } private static File getPhysicalFileOfJBossVfsJar(URL url) throws IOException, VaadinInitializerException { try { Object jarVirtualFile = url.openConnection().getContent(); // Creating a temporary jar file out of the vfs files String vfsJarPath = url.toString(); String fileNamePrefix = vfsJarPath.substring( vfsJarPath.lastIndexOf('/') + 1, vfsJarPath.lastIndexOf(".jar")); Path tempJar = Files.createTempFile(fileNamePrefix, ".jar"); generateJarFromJBossVfsFolder(jarVirtualFile, tempJar); File tempJarFile = tempJar.toFile(); tempJarFile.deleteOnExit(); return tempJarFile; } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException exc) { throw new VaadinInitializerException( "Failed to invoke JBoss VFS API.", exc); } } private static void generateJarFromJBossVfsFolder(Object jarVirtualFile, Path tempJar) throws IOException, IllegalAccessException, InvocationTargetException, NoSuchMethodException { // We should use reflection to use JBoss VFS API as we cannot // afford a // dependency to WildFly or JBoss Class virtualFileClass = jarVirtualFile.getClass(); Method getChildrenRecursivelyMethod = virtualFileClass .getMethod("getChildrenRecursively"); Method openStreamMethod = virtualFileClass.getMethod("openStream"); Method isFileMethod = virtualFileClass.getMethod("isFile"); Method getPathNameRelativeToMethod = virtualFileClass .getMethod("getPathNameRelativeTo", virtualFileClass); List jarVirtualChildren = (List) getChildrenRecursivelyMethod .invoke(jarVirtualFile); try (ZipOutputStream zipOutputStream = new ZipOutputStream( Files.newOutputStream(tempJar))) { for (Object child : jarVirtualChildren) { if (!(Boolean) isFileMethod.invoke(child)) continue; String relativePath = (String) getPathNameRelativeToMethod .invoke(child, jarVirtualFile); InputStream inputStream = (InputStream) openStreamMethod .invoke(child); ZipEntry zipEntry = new ZipEntry(relativePath); zipOutputStream.putNextEntry(zipEntry); IOUtils.copy(inputStream, zipOutputStream); zipOutputStream.closeEntry(); } } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy