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

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

There is a newer version: 24.6.2
Show newest version
/*
 * Copyright 2000-2023 Vaadin Ltd.
 *
 * 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 com.vaadin.base.devserver.startup;

import jakarta.servlet.annotation.HandlesTypes;

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.Arrays;
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 org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.vaadin.base.devserver.ViteHandler;
import com.vaadin.base.devserver.stats.DevModeUsageStatistics;
import com.vaadin.base.devserver.stats.StatisticsSender;
import com.vaadin.base.devserver.stats.StatisticsStorage;
import com.vaadin.base.devserver.viteproxy.ViteWebsocketEndpoint;
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.Mode;
import com.vaadin.flow.server.VaadinContext;
import com.vaadin.flow.server.VaadinServlet;
import com.vaadin.flow.server.frontend.FrontendUtils;
import com.vaadin.flow.server.frontend.NodeTasks;
import com.vaadin.flow.server.frontend.Options;
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 com.vaadin.pro.licensechecker.LicenseChecker;

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

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_FRONTEND_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;

/**
 * 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; } // This needs to be set as there is no "current service" available in // this call FeatureFlags featureFlags = FeatureFlags.get(context); LicenseChecker.setStrictOffline(true); featureFlags.setPropertiesLocation(config.getJavaResourceFolder()); File baseDir = config.getProjectFolder(); // Initialize the usage statistics if enabled if (config.isUsageStatisticsEnabled()) { StatisticsStorage storage = new StatisticsStorage(); DevModeUsageStatistics.init(baseDir, storage, new StatisticsSender(storage)); } String frontendFolder = config.getStringProperty(PARAM_FRONTEND_DIR, System.getProperty(PARAM_FRONTEND_DIR, DEFAULT_FRONTEND_DIR)); Lookup lookupFromContext = context.getAttribute(Lookup.class); Lookup lookupForClassFinder = Lookup.of(new DevModeClassFinder(classes), ClassFinder.class); Lookup lookup = Lookup.compose(lookupForClassFinder, lookupFromContext); Options options = new Options(lookup, baseDir) .withFrontendDirectory(new File(frontendFolder)) .withBuildDirectory(config.getBuildFolder()); log().info("Starting dev-mode updaters in {} folder.", options.getNpmFolder()); // Regenerate Vite 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()); options.withWebpack( Paths.get(target.getPath(), "classes", VAADIN_WEBAPP_RESOURCES) .toFile(), Paths.get(target.getPath(), "classes", VAADIN_SERVLET_RESOURCES) .toFile()); // If we are missing either the base or generated package json // files generate those if (!new File(options.getNpmFolder(), PACKAGE_JSON).exists()) { options.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[] additionalPostinstallPackages = config .getStringProperty( InitParameters.ADDITIONAL_POSTINSTALL_PACKAGES, "") .split(","); String frontendGeneratedFolderName = config .getStringProperty(PROJECT_FRONTEND_GENERATED_DIR_TOKEN, Paths.get(baseDir.getAbsolutePath(), DEFAULT_PROJECT_FRONTEND_GENERATED_DIR) .toString()); File frontendGeneratedFolder = new File(frontendGeneratedFolderName); File jarFrontendResourcesFolder = new File(frontendGeneratedFolder, FrontendUtils.JAR_RESOURCES_FOLDER); JsonObject tokenFileData = Json.createObject(); Mode mode = config.getMode(); options.enablePackagesUpdate(true) .useByteCodeScanner(useByteCodeScanner) .withFrontendGeneratedFolder(frontendGeneratedFolder) .withJarFrontendResourcesFolder(jarFrontendResourcesFolder) .copyResources(frontendLocations) .copyLocalResources(new File(baseDir, Constants.LOCAL_FRONTEND_RESOURCES_PATH)) .enableImportsUpdate(true) .withRunNpmInstall(mode == Mode.DEVELOPMENT_FRONTEND_LIVERELOAD) .populateTokenFileData(tokenFileData) .withEmbeddableWebComponents(true).withEnablePnpm(enablePnpm) .useGlobalPnpm(useGlobalPnpm) .withHomeNodeExecRequired(useHomeNodeExec) .withProductionMode(config.isProductionMode()) .withPostinstallPackages( Arrays.asList(additionalPostinstallPackages)) .withFrontendHotdeploy( mode == Mode.DEVELOPMENT_FRONTEND_LIVERELOAD) .withBundleBuild(mode == Mode.DEVELOPMENT_BUNDLE); NodeTasks tasks = new NodeTasks(options); Runnable runnable = () -> { runNodeTasks(context, tokenFileData, tasks); if (mode == Mode.DEVELOPMENT_FRONTEND_LIVERELOAD) { // For Vite, wait until a VaadinServlet is deployed so we know // which frontend servlet path to use if (VaadinServlet.getFrontendMapping() == null) { log().debug("Waiting for a VaadinServlet to be deployed"); while (VaadinServlet.getFrontendMapping() == null) { try { Thread.sleep(100); } catch (InterruptedException e) { } } } } }; CompletableFuture nodeTasksFuture = CompletableFuture .runAsync(runnable); Lookup devServerLookup = Lookup.compose(lookup, Lookup.of(config, ApplicationConfiguration.class)); int port = Integer .parseInt(config.getStringProperty("devServerPort", "0")); if (mode == Mode.DEVELOPMENT_BUNDLE) { nodeTasksFuture.join(); return null; } else { ViteHandler handler = new ViteHandler(devServerLookup, port, options.getNpmFolder(), nodeTasksFuture); VaadinServlet.whenFrontendMappingAvailable( () -> ViteWebsocketEndpoint.init(context, handler)); return handler; } } private static Logger log() { return LoggerFactory.getLogger(DevModeStartupListener.class); } /* * 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(); } 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