From 837680b077b16e6a6b834349ef30ab96a5b14bad Mon Sep 17 00:00:00 2001 From: Robinson Date: Fri, 17 Sep 2021 17:46:23 +0200 Subject: [PATCH] Better support for directly serving files we know about at startup. Fixed bug with overwriting temp files. Fixed bug with Trie when there is an empty map used to construct it --- src/dorkbox/vaadin/VaadinApplication.kt | 520 ++++++++++-------- src/dorkbox/vaadin/compiler/VaadinCompile.kt | 11 + .../vaadin/undertow/JarResourceManager.kt | 28 +- .../undertow/StrictFileResourceManager.kt | 75 +++ src/dorkbox/vaadin/undertow/WebResource.kt | 5 + .../vaadin/undertow/WebResourceString.kt | 5 + src/dorkbox/vaadin/util/VaadinConfig.kt | 107 ++++ .../util/ahoCorasick/DoubleArrayTrie.kt | 28 +- 8 files changed, 529 insertions(+), 250 deletions(-) create mode 100644 src/dorkbox/vaadin/compiler/VaadinCompile.kt create mode 100644 src/dorkbox/vaadin/undertow/StrictFileResourceManager.kt create mode 100644 src/dorkbox/vaadin/undertow/WebResource.kt create mode 100644 src/dorkbox/vaadin/undertow/WebResourceString.kt create mode 100644 src/dorkbox/vaadin/util/VaadinConfig.kt diff --git a/src/dorkbox/vaadin/VaadinApplication.kt b/src/dorkbox/vaadin/VaadinApplication.kt index 44825c7..96fe55d 100644 --- a/src/dorkbox/vaadin/VaadinApplication.kt +++ b/src/dorkbox/vaadin/VaadinApplication.kt @@ -1,15 +1,12 @@ package dorkbox.vaadin -import com.vaadin.flow.server.Constants -import com.vaadin.flow.server.InitParameters import com.vaadin.flow.server.VaadinServlet import com.vaadin.flow.server.frontend.FrontendUtils import dorkbox.vaadin.devMode.DevModeInitializer import dorkbox.vaadin.undertow.* import dorkbox.vaadin.util.CallingClass +import dorkbox.vaadin.util.VaadinConfig import dorkbox.vaadin.util.ahoCorasick.DoubleArrayTrie -import elemental.json.JsonObject -import elemental.json.impl.JsonUtil import io.github.classgraph.ClassGraph import io.github.classgraph.ScanResult import io.undertow.Undertow @@ -20,7 +17,6 @@ import io.undertow.server.HttpServerExchange import io.undertow.server.handlers.cache.CacheHandler import io.undertow.server.handlers.cache.DirectBufferCache import io.undertow.server.handlers.resource.CachingResourceManager -import io.undertow.server.handlers.resource.FileResourceManager import io.undertow.server.handlers.resource.ResourceManager import io.undertow.servlet.Servlets import io.undertow.servlet.api.ServletContainerInitializerInfo @@ -51,6 +47,8 @@ import kotlin.reflect.KClass */ class VaadinApplication() { companion object { + const val debugResources = true + /** * Gets the version number. */ @@ -62,15 +60,14 @@ class VaadinApplication() { } } - val runningAsJar: Boolean private val logger = KotlinLogging.logger {} + + val runningAsJar: Boolean val tempDir: File = File(System.getProperty("java.io.tmpdir", "tmpDir"), "undertow").absoluteFile private val onStopList = mutableListOf() - val devMode: Boolean - private val tokenFileName: String - val pNpmEnabled: Boolean + val vaadinConfig: VaadinConfig private lateinit var urlClassLoader: URLClassLoader private lateinit var resourceCollectionManager: ResourceCollectionManager @@ -91,62 +88,7 @@ class VaadinApplication() { runningAsJar = CallingClass.get().getResource("")!!.protocol == "jar" - // find the config/stats.json to see what mode (PRODUCTION or DEV) we should run in. - // we COULD just check the existence of this file... - // HOWEVER if we are testing a different configuration from our IDE, this method will not work... - var tokenJson: JsonObject? = null - - val defaultTokenFile = "VAADIN/${FrontendUtils.TOKEN_FILE}" - // token location if we are running in a jar - val tokenInJar = this.javaClass.classLoader.getResource("META-INF/resources/$defaultTokenFile") - if (tokenInJar != null) { - tokenFileName = if (runningAsJar) { - // the token file name MUST always be from disk! This is hard coded, because later we copy out - // this file from the jar to the temp location. - File(tempDir, defaultTokenFile).absolutePath - } else { - if (tokenInJar.path.startsWith("/")) { - tokenInJar.path.substring(1) - } else { - tokenInJar.path - } - } - - tokenJson = JsonUtil.parse(tokenInJar.readText(Charsets.UTF_8)) as JsonObject? - } else { - // maybe the token file is in the temp build location (used during dev work). - val devTokenFile = File("build").resolve("resources").resolve("main").resolve("META-INF").resolve("resources").resolve("VAADIN").resolve(FrontendUtils.TOKEN_FILE) - if (devTokenFile.canRead()) { - tokenFileName = devTokenFile.absoluteFile.normalize().path - tokenJson = JsonUtil.parse(File(tokenFileName).readText(Charsets.UTF_8)) as JsonObject? - } - else { - tokenFileName = "" - } - } - - if (tokenFileName.isEmpty() || tokenJson == null || !tokenJson.hasKey(InitParameters.SERVLET_PARAMETER_PRODUCTION_MODE)) { - // this is a problem! we must configure the system first via gradle! - throw java.lang.RuntimeException("Unable to continue! Error reading token!" + - "You must FIRST compile the vaadin resources for DEV or PRODUCTION mode!") - } - - devMode = !tokenJson.getBoolean(InitParameters.SERVLET_PARAMETER_PRODUCTION_MODE) - pNpmEnabled = tokenJson.getBoolean(InitParameters.SERVLET_PARAMETER_ENABLE_PNPM) - - if (devMode && runningAsJar) { - throw RuntimeException("Invalid run configuration. It is not possible to run DEV MODE from a deployed jar.\n" + - "Something is severely wrong!") - } - - // we are ALWAYS running in full Vaadin14 mode - System.setProperty(Constants.VAADIN_PREFIX + InitParameters.SERVLET_PARAMETER_COMPATIBILITY_MODE, "false") - - if (devMode) { - // set the location of our frontend dir + generated dir when in dev mode - System.setProperty(FrontendUtils.PARAM_FRONTEND_DIR, tokenJson.getString(Constants.FRONTEND_TOKEN)) - System.setProperty(FrontendUtils.PARAM_GENERATED_DIR, tokenJson.getString(Constants.GENERATED_TOKEN)) - } + vaadinConfig = VaadinConfig(runningAsJar, tempDir) } private fun addAnnotated(annotationScanner: ScanResult, @@ -173,12 +115,32 @@ class VaadinApplication() { } } + private fun recurseAllFiles(allRelativeFilePaths: MutableSet, file: File, rootFileSize: Int) { + file.listFiles()?.forEach { + if (it.isDirectory) { + recurseAllFiles(allRelativeFilePaths, it, rootFileSize) + } else { + val resourcePath = it.toURI().toURL() + + // must ALSO use forward slash (unix)!! + val relativePath = resourcePath.path.substring(rootFileSize) + + logger.error { + "Disk resource: $relativePath" + } + + allRelativeFilePaths.add(WebResource(relativePath, resourcePath)) + } + } + } + @Suppress("DuplicatedCode") fun initResources() { val metaInfResources = "META-INF/resources" - val buildMetaInfResources = "build/resources/main/META-INF/resources" val metaInfValLength = metaInfResources.length + 1 + val buildMetaInfResources = "build/resources/main/META-INF/resources" + // resource locations are tricky... // when a JAR : META-INF/resources @@ -206,25 +168,38 @@ class VaadinApplication() { .scan() - val jarLocations = mutableSetOf() - val diskLocations = mutableSetOf() + val jarLocations = mutableSetOf() + val diskLocations = mutableSetOf() + val urlClassLoader = mutableSetOf() - if (runningAsJar) { - // collect all the resources available. - logger.info { "Extracting all jar $metaInfResources files to $tempDir" } + if (runningAsJar && vaadinConfig.extractFromJar) { + // running from JAR (really just temp-dir, since the jar is extracted!) + logger.info { "Running from jar and extracting all jar [$metaInfResources] files to [$tempDir]" } + var lastFile: File? = null scanResultJarDependencies.allResources.forEach { resource -> val resourcePath = resource.pathRelativeToClasspathElement val relativePath = resourcePath.substring(metaInfValLength) - logger.trace { - "Discovered resource: $relativePath" +// // we only care about VAADIN and frontend resources for JARs -- everything else is compiled as part of the webpack process. +// val lower = relativePath.lowercase(Locale.US) +// if (!lower.startsWith("vaadin") && !lower.startsWith("frontend")) { +// logger.trace { "Skipping classpath resource: $relativePath" } +// return@forEach +// } + + logger.error { "Jar resource: $relativePath" } + if (lastFile != resource.classpathElementFile) { + lastFile = resource.classpathElementFile + logger.error { "Jar resource: ${resource.classpathElementFile}" } } // we should copy this resource out, since loading resources from jar files is time+memory intensive - val outputFile = File(tempDir, relativePath) + val outputFile = tempDir.resolve(relativePath) - if (!outputFile.exists()) { +// // TODO: should overwrite file? check hashes? + // if there is ever a NEW version of our code run, the OLD version will still run if the files are not overwritten! +// if (!outputFile.exists()) { val parentFile = outputFile.parentFile if (!parentFile.isDirectory && !parentFile.mkdirs()) { logger.error { "Unable to create output directory $parentFile" } @@ -233,148 +208,254 @@ class VaadinApplication() { outputFile.outputStream().use { input.copyTo(it) } } } - } +// } + + diskLocations.add(WebResource(relativePath, outputFile.toURI().toURL())) } - val locations = arrayOf(tempDir.toURI().toURL()) - jarLocations.addAll(locations) - // so we can use the undertow cache to serve resources, instead of the vaadin servlet (which doesn't cache, and is really slow) - urlClassLoader = object : URLClassLoader(locations, this.javaClass.classLoader) { - override fun getResource(name: String): URL? { - if (name.startsWith("META-INF")) { - // the problem is that: - // request is : META-INF/VAADIN/build/webcomponentsjs/webcomponents-loader.js - // resource is: VAADIN/build/webcomponentsjs/webcomponents-loader.js + } else if (runningAsJar) { + // running from JAR (not-extracted!) + logger.info { "Running from jar files" } - val fixedName = name.substring("META-INF".length) - return super.getResource(fixedName) - } + var lastFile: File? = null + scanResultJarDependencies.allResources.forEach { resource -> + val resourcePath = resource.pathRelativeToClasspathElement + val relativePath = resourcePath.substring(metaInfValLength) - return super.getResource(name) +// // we only care about VAADIN and frontend resources for JARs -- everything else is compiled as part of the webpack process. +// val lower = relativePath.lowercase(Locale.US) +// if (!lower.startsWith("vaadin") && !lower.startsWith("frontend")) { +// logger.trace { "Skipping JAR resource: $relativePath" } +// return@forEach +// } + + logger.error { "Jar resource: $relativePath" } + if (lastFile != resource.classpathElementFile) { + lastFile = resource.classpathElementFile + logger.error { "Jar resource: ${resource.classpathElementFile}" } } + + // these are all resources inside JAR files. + jarLocations.add(WebResourceString(relativePath, resource.classpathElementURL, resourcePath)) + + // jar file this resource is from -- BUT NOT THE RESOURCE ITSELF + urlClassLoader.add(resource.classpathElementURL) } - } - else { - // when we are running in DISK (aka, not-running-as-a-jar) mode, we are NOT extracting all of the resources to a temp location. - // BECAUSE of this, we must create a MAP of the RELATIVE resource name --> ABSOLUTE resource name - // This is so our classloader can find the resource without having to manually configure each requests. - val jarResourceRequestMap = TreeMap() - val diskResourceRequestMap = TreeMap() + } else { + // running from IDE + logger.info { "Running from IDE files" } + + var lastFile: File? = null scanResultJarDependencies.allResources.forEach { resource -> val resourcePath = resource.pathRelativeToClasspathElement val relativePath = resourcePath.substring(metaInfValLength) logger.error { "Jar resource: $relativePath" } + if (lastFile != resource.classpathElementFile) { + lastFile = resource.classpathElementFile + logger.error { "Jar resource: ${resource.classpathElementFile}" } + } - jarLocations.add(resource.classpathElementURL) + // these are all resources inside JAR files. + jarLocations.add(WebResourceString(relativePath, resource.url, resourcePath)) - jarResourceRequestMap[relativePath] = resourcePath - // some-of the resources are loaded with a "META-INF" prefix by the vaadin servlet - jarResourceRequestMap["META-INF/$relativePath"] = resourcePath + // jar file this resource is from -- BUT NOT THE RESOURCE ITSELF + urlClassLoader.add(resource.classpathElementURL) } - // some static resources from disk are ALSO loaded by the classloader. scanResultLocalDependencies.allResources.forEach { resource -> val resourcePath = resource.pathRelativeToClasspathElement val relativePath = resourcePath.substring(metaInfValLength) - logger.error { "Classpath resource: $relativePath" } + logger.error { "Local resource: $relativePath" } - diskLocations.add(resource.classpathElementURL) - - val url = resource.url - diskResourceRequestMap[relativePath] = url - - // some-of the resources are loaded with a "META-INF" prefix by the vaadin servlet - diskResourceRequestMap["META-INF/$relativePath"] = url + diskLocations.add(WebResource(relativePath, resource.url)) } // we also have resources that are OUTSIDE the classpath (ie: in the temp build dir) // This is necessary BECAUSE we have to be able to ALSO serve resources via the classloader! val buildDirMetaInfResources = File(buildMetaInfResources).absoluteFile.normalize() val rootPathLength = buildDirMetaInfResources.toURI().toURL().path.length - recurseAllFiles(diskResourceRequestMap, buildDirMetaInfResources, rootPathLength) + recurseAllFiles(diskLocations, buildDirMetaInfResources, rootPathLength) + } + // we use the TRIE data structure to QUICKLY find what we are looking for. + // This is so our classloader can find the resource without having to manually configure each request. + val jarStringResourceRequestMap = mutableMapOf() + val jarUrlResourceRequestMap = mutableMapOf() + val diskResourceRequestMap = mutableMapOf() - // so we can use the undertow cache to serve resources, instead of the vaadin servlet (which doesn't cache, and is really slow) - // NOTE: this is what ALSO will load the stats.json file! - urlClassLoader = object : URLClassLoader(jarLocations.toTypedArray(), this.javaClass.classLoader) { - val jarTrie = DoubleArrayTrie(jarResourceRequestMap) - val diskTrie = DoubleArrayTrie(diskResourceRequestMap) + jarLocations.forEach { (requestPath, resourcePath, relativeResourcePath) -> + // make sure the path is WWW request compatible (ie: no spaces/etc) + val wwwCompatiblePath = java.net.URLDecoder.decode(requestPath, Charsets.UTF_8) - override fun getResource(name: String): URL? { - // check disk first - val diskResourcePath = diskTrie[name] - if (diskResourcePath != null) { - return diskResourcePath - } + // this adds the resource to our request map, used by our trie + jarStringResourceRequestMap[wwwCompatiblePath] = relativeResourcePath + jarUrlResourceRequestMap[wwwCompatiblePath] = resourcePath - val jarResourcePath = jarTrie[name] - if (jarResourcePath != null) { - return super.getResource(jarResourcePath) - } - - return super.getResource(name) - } + if (!wwwCompatiblePath.startsWith("META-INF")) { + // some-of the resources are loaded with a "META-INF" prefix by the vaadin servlet + jarStringResourceRequestMap["META-INF/$wwwCompatiblePath"] = relativeResourcePath + jarUrlResourceRequestMap["META-INF/$wwwCompatiblePath"] = resourcePath } - // we also have resources that are OUTSIDE the classpath (ie: in the temp build dir) - diskLocations.add(buildDirMetaInfResources.toURI().toURL()) + if (!wwwCompatiblePath.startsWith('/')) { + // some-of the resources are loaded with a "/" prefix by the vaadin servlet + jarStringResourceRequestMap["/$wwwCompatiblePath"] = relativeResourcePath + jarUrlResourceRequestMap["/$wwwCompatiblePath"] = resourcePath + } } + diskLocations.forEach { (requestPath, resourcePath) -> + // make sure the path is WWW request compatible (ie: no spaces/etc) + val wwwCompatiblePath = java.net.URLDecoder.decode(requestPath, Charsets.UTF_8) + + // this adds the resource to our request map, used by our trie + diskResourceRequestMap[wwwCompatiblePath] = resourcePath + + if (!wwwCompatiblePath.startsWith("META-INF")) { + // some-of the resources are loaded with a "META-INF" prefix by the vaadin servlet + diskResourceRequestMap["META-INF/$wwwCompatiblePath"] = resourcePath + } + + if (!wwwCompatiblePath.startsWith('/')) { + // some-of the resources are loaded with a "/" prefix by the vaadin servlet + diskResourceRequestMap["/$wwwCompatiblePath"] = resourcePath + } + } + + // EVERYTHING IS ACCESSED VIA TRIE, NOT VIA HASHMAP! (it's faster this way) + val jarStringTrie = DoubleArrayTrie(jarStringResourceRequestMap) + val jarUrlTrie = DoubleArrayTrie(jarUrlResourceRequestMap) + val diskTrie = DoubleArrayTrie(diskResourceRequestMap) + + //URL Classloader: META-INF/VAADIN/build/vaadin-bundle-4d7dbedf0dba552475bc.cache.js + // so we can use the undertow cache to serve resources, instead of the vaadin servlet (which doesn't cache, and is really slow) + // NOTE: this will load the stats.json file! + val toTypedArray = jarLocations.map { it.resourcePath }.toTypedArray() + this.urlClassLoader = object : URLClassLoader(toTypedArray, this.javaClass.classLoader) { + override fun getResource(name: String): URL? { + if (debugResources) { + println(" URL Classloader: $name") + } + + // check disk first + val diskResourcePath: URL? = diskTrie[name] + if (diskResourcePath != null) { + if (debugResources) { + println("TRIE: $diskResourcePath") + } + return diskResourcePath + } + + val jarResourcePath: String? = jarStringTrie[name] + if (jarResourcePath != null) { + if (debugResources) { + println("TRIE: $jarResourcePath") + } + return super.getResource(jarResourcePath) + } + + return super.getResource(name) + } + } + + val strictFileResourceManager = StrictFileResourceManager("Static Files", diskTrie) + val jarResourceManager = JarResourceManager("Jar Files", jarUrlTrie) + +// val jarResources = ArrayList() +// jarLocations.forEach { (requestPath, resourcePath, relativeResourcePath) -> +//// val cleanedUrl = java.net.URLDecoder.decode(jarUrl.file, Charsets.UTF_8) +// val file = File(resourcePath.file) +// +// if (debugResources) { +// println(" JAR: $file") +// } +// +// // the location IN THE JAR is actually "META-INF/resources", so we want to make sure of that when +// // serving the request, that the correct path is used. +// jarResources.add(JarResourceManager(file, metaInfResources)) +// } + + // collect all the resources available from each location to ALSO be handled by undertow - val diskResources = ArrayList() - val jarResources = ArrayList() - val fileResources = ArrayList() +// val diskResources = ArrayList() +// val fileResources = ArrayList() - jarLocations.forEach { jarUrl -> - val cleanedUrl = java.net.URLDecoder.decode(jarUrl.file, Charsets.UTF_8) - val file = File(cleanedUrl) - // the location IN THE JAR is actually "META-INF/resources", so we want to make sure of that when - // serving the request, that the correct path is used. - jarResources.add(JarResourceManager(file, metaInfResources)) - } - diskLocations.forEach { diskUrl -> - val cleanedUrl = java.net.URLDecoder.decode(diskUrl.file, Charsets.UTF_8) - val file = File(cleanedUrl) - // if this location is where our "META-INF/resources" directory exists, ALSO add that location, because the - // vaadin will request resources based on THAT location as well. - val metaInfResourcesLocation = File(file, metaInfResources) - if (metaInfResourcesLocation.isDirectory) { - diskResources.add(FileResourceManager(metaInfResourcesLocation)) - - // we will also serve content from ALL child directories - metaInfResourcesLocation.listFiles()?.forEach { childFile -> - when { - childFile.isDirectory -> prefixResources.add("/${childFile.relativeTo(metaInfResourcesLocation)}") - else -> exactResources.add("/${childFile.relativeTo(metaInfResourcesLocation)}") - } - } - } - - diskResources.add(FileResourceManager(file)) - - // we will also serve content from ALL child directories - // (except for the META-INF dir, which we are ALREADY serving content) - file.listFiles()?.forEach { childFile -> - when { - childFile.isDirectory -> { - if (childFile.name != "META-INF") { - prefixResources.add("/${childFile.relativeTo(file)}") - } - } - else -> exactResources.add("/${childFile.relativeTo(file)}") - } - } - } +// diskLocations.forEach { (requestPath, resourcePath) -> +// val wwwCompatiblePath = java.net.URLDecoder.decode(requestPath, Charsets.UTF_8) +// val diskFile = resourcePath.file +// +// +// // this serves a BASE location! +// diskResources.add(FileResourceManager(metaInfResourcesLocation)) +// +// // if this location is where our "META-INF/resources" directory exists, ALSO add that location, because the +// // vaadin will request resources based on THAT location as well. +// val metaInfResourcesLocation = File(file, metaInfResources) +// +// if (metaInfResourcesLocation.isDirectory) { +// diskResources.add(FileResourceManager(metaInfResourcesLocation)) +// +// // we will also serve content from ALL child directories +// metaInfResourcesLocation.listFiles()?.forEach { childFile -> +// val element = "/${childFile.relativeTo(metaInfResourcesLocation)}" +// if (debugResources) { +// println(" DISK: $cleanedUrl") +// } +// +// when { +// childFile.isDirectory -> prefixResources.add(element) +// else -> exactResources.add(element) +// } +// } +// } +// +// if (debugResources) { +// println(" DISK: $cleanedUrl") +// } +// +// diskResources.add(FileResourceManager(file)) +// +// // we will also serve content from ALL child directories +// // (except for the META-INF dir, which we are ALREADY serving content) +// file.listFiles()?.forEach { childFile -> +// val element = "/${childFile.relativeTo(file)}" +// +// if (debugResources) { +// println(" DISK: $element") +// } +// +// when { +// childFile.isDirectory -> { +// if (childFile.name != "META-INF") { +// prefixResources.add(element) +// } +// } +// else -> exactResources.add(element) +// } +// } +// } +// jarLocations.forEach { jarUrl -> +// val cleanedUrl = java.net.URLDecoder.decode(jarUrl.file, Charsets.UTF_8) +// val file = File(cleanedUrl) +// +// if (debugResources) { +// println(" JAR: $cleanedUrl") +// } +// +// // the location IN THE JAR is actually "META-INF/resources", so we want to make sure of that when +// // serving the request, that the correct path is used. +// jarResources.add(JarResourceManager(file, metaInfResources)) +// } // When we are searching for resources, the following search order is optimized for access speed and request hit order // DISK @@ -384,62 +465,39 @@ class VaadinApplication() { // flow-push // flow-server // then every other jar - resources.addAll(diskResources) - resources.addAll(fileResources) + resources.add(strictFileResourceManager) + resources.add(jarResourceManager) +// resources.addAll(diskResources) +// resources.addAll(fileResources) - val client = jarResources.firstOrNull { it.name.contains("flow-client") } - val push = jarResources.firstOrNull { it.name.contains("flow-push") } - val server = jarResources.firstOrNull { it.name.contains("flow-server") } - if (client != null && push != null && server != null) { - // these jars will ALWAYS be available (as of Vaadin 14.2) - // if we are running from a fatjar, then the resources will likely be extracted (so this is not necessary) - jarResources.remove(client) - jarResources.remove(push) - jarResources.remove(server) - - resources.add(client) - resources.add(push) - resources.add(server) - } - - resources.addAll(jarResources) +// val client = jarResources.firstOrNull { it.name.contains("flow-client") } +// val push = jarResources.firstOrNull { it.name.contains("flow-push") } +// val server = jarResources.firstOrNull { it.name.contains("flow-server") } +// +// if (client != null && push != null && server != null) { +// // these jars will ALWAYS be available (as of Vaadin 14.2) +// // if we are running from a fatjar, then the resources will likely be extracted (so this is not necessary) +// jarResources.remove(client) +// jarResources.remove(push) +// jarResources.remove(server) +// +// resources.add(client) +// resources.add(push) +// resources.add(server) +// } +// +// resources.addAll(jarResources) // TODO: Have a 404 resource handler to log when a requested file is not found! // NOTE: atmosphere is requesting the full path of 'WEB-INF/classes/'. - // What do to? search this with classgraph OR we re-map this to 'out/production/classes/' ?? + // What to do? search this with classgraph OR we re-map this to 'out/production/classes/' ?? // also accessed is : WEB-INF/lib/ // TODO: runtime GZ compression of resources!?! only necessary in the JAR run mode (which is what runs on servers) } - // Does not add to locations! - private fun recurseAllFiles( - resourceRequestMap: TreeMap, - file: File, - rootFileSize: Int - ) { - file.listFiles()?.forEach { - if (it.isDirectory) { - recurseAllFiles(resourceRequestMap, it, rootFileSize) - } else { - val resourcePath = it.toURI().toURL() - - // must ALSO use forward slash (unix)!! - val relativePath = resourcePath.path.substring(rootFileSize) - - logger.error { - "Disk resource: $relativePath" - } - - resourceRequestMap[relativePath] = resourcePath - // some-of the resources are loaded with a "META-INF" prefix by the vaadin servlet - resourceRequestMap["META-INF/$relativePath"] = resourcePath - } - } - } - fun initServlet(enableCachedHandlers: Boolean, cacheTimeoutSeconds: Int, servletClass: Class = VaadinServlet::class.java, servletName: String = "Vaadin", @@ -473,20 +531,15 @@ class VaadinApplication() { .setLoadOnStartup(1) .setAsyncSupported(true) .setExecutor(null) // we use coroutines! - .addInitParam("productionMode", (!devMode).toString()) // this is set via the gradle build // have to say where our NPM/dev mode files live. .addInitParam(FrontendUtils.PROJECT_BASEDIR, File("").absolutePath) - // have to say where our token file lives - .addInitParam(FrontendUtils.PARAM_TOKEN_FILE, tokenFileName) - - // where our stats.json file lives. This loads via classloader, not via a file!! - .addInitParam(InitParameters.SERVLET_PARAMETER_STATISTICS_JSON, "VAADIN/config/stats.json") - .addInitParam("enable-websockets", "true") .addMapping("/*") + vaadinConfig.addServletInitParameters(servlet) + // setup (or change) custom config options ( servletConfig(servlet) @@ -536,7 +589,7 @@ class VaadinApplication() { ClassGraph().enableAnnotationInfo().enableClassInfo().scan().use { annotationScanner -> for (service in serviceLoader) { - val classSet = hashSetOf>() + val classSet: HashSet> = hashSetOf() val javaClass = service.javaClass val annotation= javaClass.getAnnotation(HandlesTypes::class.java) @@ -549,7 +602,7 @@ class VaadinApplication() { if (classSet.isNotEmpty()) { if (javaClass == com.vaadin.flow.server.startup.DevModeInitializer::class.java) { - if (devMode) { + if (vaadinConfig.devMode) { // instead of the default, we load **OUR** dev-mode initializer. // The vaadin one is super buggy for custom environments servletBuilder.addServletContainerInitializer( @@ -596,7 +649,7 @@ class VaadinApplication() { */ - if (devMode) { + if (vaadinConfig.devMode) { // NOTE: The vaadin flow files only exist AFTER vaadin is initialized, so this block MUST be after 'manager.deploy()' // in dev mode, the local resources are hardcoded to an **INCORRECT** location. They // are hardcoded to "src/main/resources/META-INF/resources/frontend", so we have to @@ -651,7 +704,11 @@ class VaadinApplication() { } fun handleRequest(exchange: HttpServerExchange) { + // dev-mode : incoming requests USUALLY start with a '/' val path = exchange.relativePath + if (debugResources) { + println("REQUEST undertow: $path") + } // serve the following directly via the resource handler, so we can do it directly in the networking IO thread. // Because this is non-blocking, this is also the preferred way to do this for performance. @@ -743,7 +800,12 @@ class VaadinApplication() { // servletWebapp.destroy() // allChannels.close().awaitUninterruptibly() - worker?.shutdown() // maybe? + val worker = worker + if (worker != null) { + worker.shutdown() + worker.awaitTermination(10L, TimeUnit.SECONDS) + } + undertowServer?.stop() } finally { onStopList.forEach { diff --git a/src/dorkbox/vaadin/compiler/VaadinCompile.kt b/src/dorkbox/vaadin/compiler/VaadinCompile.kt new file mode 100644 index 0000000..da6bb01 --- /dev/null +++ b/src/dorkbox/vaadin/compiler/VaadinCompile.kt @@ -0,0 +1,11 @@ +package dorkbox.vaadin.compiler + +/** + * + */ +object VaadinCompile { + fun print() { + println("vaadin compile from maven") + } + +} diff --git a/src/dorkbox/vaadin/undertow/JarResourceManager.kt b/src/dorkbox/vaadin/undertow/JarResourceManager.kt index e13be5a..57c0471 100644 --- a/src/dorkbox/vaadin/undertow/JarResourceManager.kt +++ b/src/dorkbox/vaadin/undertow/JarResourceManager.kt @@ -15,12 +15,13 @@ */ package dorkbox.vaadin.undertow +import dorkbox.vaadin.VaadinApplication +import dorkbox.vaadin.util.ahoCorasick.DoubleArrayTrie import io.undertow.UndertowMessages import io.undertow.server.handlers.resource.Resource import io.undertow.server.handlers.resource.ResourceChangeListener import io.undertow.server.handlers.resource.ResourceManager import io.undertow.server.handlers.resource.URLResource -import java.io.File import java.io.IOException import java.net.URL @@ -31,26 +32,19 @@ import java.net.URL * @author Andy Wilkinson * @author Dorkbox LLC */ -internal class JarResourceManager(val jarFile: String, base: String = "") : ResourceManager { - - val name: String by lazy { File(jarFile).nameWithoutExtension } - - constructor(jarFile: File, base: String = "") : this(jarFile.absolutePath, base) - - private val fixedBase = when { - base.startsWith("/") -> base - base.isNotBlank() -> "/$base" - else -> "" - } +internal class JarResourceManager(val name: String, val trie: DoubleArrayTrie) : ResourceManager { @Throws(IOException::class) override fun getResource(path: String): Resource? { - val cleanedPath = when { - path.startsWith("/") -> path - else -> "/$path" + if (VaadinApplication.debugResources) { + println("REQUEST jar: $path") } - val url = URL("jar:file:$jarFile!$fixedBase$cleanedPath") + val url = trie[path] ?: return null + + if (VaadinApplication.debugResources) { + println("TRIE: $url") + } val resource = URLResource(url, path) if (path.isNotBlank() && path != "/" && resource.contentLength < 0) { @@ -77,6 +71,6 @@ internal class JarResourceManager(val jarFile: String, base: String = "") : Reso } override fun toString(): String { - return "JarResourceManager($jarFile)" + return "JarResourceManager($name)" } } diff --git a/src/dorkbox/vaadin/undertow/StrictFileResourceManager.kt b/src/dorkbox/vaadin/undertow/StrictFileResourceManager.kt new file mode 100644 index 0000000..0d3f454 --- /dev/null +++ b/src/dorkbox/vaadin/undertow/StrictFileResourceManager.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * 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 + * + * https://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 dorkbox.vaadin.undertow + +import dorkbox.vaadin.VaadinApplication +import dorkbox.vaadin.util.ahoCorasick.DoubleArrayTrie +import io.undertow.UndertowMessages +import io.undertow.server.handlers.resource.FileResource +import io.undertow.server.handlers.resource.Resource +import io.undertow.server.handlers.resource.ResourceChangeListener +import io.undertow.server.handlers.resource.ResourceManager +import java.io.File +import java.io.IOException +import java.net.URL + +/** + * [ResourceManager] for pre-scanned file resources. + * + * @author Dorkbox LLC + */ +internal class StrictFileResourceManager(val name: String, val trie: DoubleArrayTrie) : io.undertow.server.handlers.resource.FileResourceManager(File(".")) { + + @Throws(IOException::class) + override fun getResource(path: String): Resource? { + if (VaadinApplication.debugResources) { + println("REQUEST static: $path") + } + + val url = trie[path] ?: return null + + if (VaadinApplication.debugResources) { + println("TRIE: $url") + } + + val resource = FileResource(File(url.file), this, path) + if (path.isNotBlank() && path != "/" && resource.contentLength < 0) { + return null + } + + return resource + } + + override fun isResourceChangeListenerSupported(): Boolean { + return false + } + + override fun registerResourceChangeListener(listener: ResourceChangeListener) { + throw UndertowMessages.MESSAGES.resourceChangeListenerNotSupported() + } + + override fun removeResourceChangeListener(listener: ResourceChangeListener) { + throw UndertowMessages.MESSAGES.resourceChangeListenerNotSupported() + } + + @Throws(IOException::class) + override fun close() { + } + + override fun toString(): String { + return "FileResourceManager($name)" + } +} diff --git a/src/dorkbox/vaadin/undertow/WebResource.kt b/src/dorkbox/vaadin/undertow/WebResource.kt new file mode 100644 index 0000000..5ec44a1 --- /dev/null +++ b/src/dorkbox/vaadin/undertow/WebResource.kt @@ -0,0 +1,5 @@ +package dorkbox.vaadin.undertow + +import java.net.URL + +data class WebResource(val requestPath: String, val resourcePath: URL) diff --git a/src/dorkbox/vaadin/undertow/WebResourceString.kt b/src/dorkbox/vaadin/undertow/WebResourceString.kt new file mode 100644 index 0000000..c5f7447 --- /dev/null +++ b/src/dorkbox/vaadin/undertow/WebResourceString.kt @@ -0,0 +1,5 @@ +package dorkbox.vaadin.undertow + +import java.net.URL + +data class WebResourceString(val requestPath: String, val resourcePath: URL, val relativeResourcePath: String) diff --git a/src/dorkbox/vaadin/util/VaadinConfig.kt b/src/dorkbox/vaadin/util/VaadinConfig.kt new file mode 100644 index 0000000..269dafb --- /dev/null +++ b/src/dorkbox/vaadin/util/VaadinConfig.kt @@ -0,0 +1,107 @@ +package dorkbox.vaadin.util + +import com.vaadin.flow.server.Constants +import com.vaadin.flow.server.InitParameters +import com.vaadin.flow.server.frontend.FrontendUtils +import elemental.json.JsonObject +import elemental.json.impl.JsonUtil +import io.undertow.servlet.api.ServletInfo +import java.io.File + +/** + * + */ +class VaadinConfig(runningAsJar: Boolean, tempDir: File) { + companion object { + val EXTRACT_JAR = "extract.jar" + val EXTRACT_JAR_OVERWRITE = "extract.jar.overwrite" + } + + val tokenFileName: String + val devMode: Boolean + val pNpmEnabled: Boolean + + // option to extract files or to load from jar only. This is a performance option. + val extractFromJar: Boolean + + // option to force files to be overwritten when `extractFromJar` is true + val forceExtractOverwrite: Boolean + + init { + // find the config/stats.json to see what mode (PRODUCTION or DEV) we should run in. + // we COULD just check the existence of this file... + // HOWEVER if we are testing a different configuration from our IDE, this method will not work... + var tokenJson: JsonObject? = null + + val defaultTokenFile = "VAADIN/${FrontendUtils.TOKEN_FILE}" + // token location if we are running in a jar + val tokenInJar = this.javaClass.classLoader.getResource("META-INF/resources/$defaultTokenFile") + if (tokenInJar != null) { + tokenFileName = if (runningAsJar) { + // the token file name MUST always be from disk! This is hard coded, because later we copy out + // this file from the jar to the temp location. + File(tempDir, defaultTokenFile).absolutePath + } else { + if (tokenInJar.path.startsWith("/")) { + tokenInJar.path.substring(1) + } else { + tokenInJar.path + } + } + + tokenJson = JsonUtil.parse(tokenInJar.readText(Charsets.UTF_8)) as JsonObject? + } else { + // maybe the token file is in the temp build location (used during dev work). + val devTokenFile = File("build").resolve("resources").resolve("main").resolve("META-INF").resolve("resources").resolve("VAADIN").resolve( + FrontendUtils.TOKEN_FILE) + if (devTokenFile.canRead()) { + tokenFileName = devTokenFile.absoluteFile.normalize().path + tokenJson = JsonUtil.parse(File(tokenFileName).readText(Charsets.UTF_8)) as JsonObject? + } + else { + tokenFileName = "" + } + } + + if (tokenFileName.isEmpty() || tokenJson == null || !tokenJson.hasKey(InitParameters.SERVLET_PARAMETER_PRODUCTION_MODE)) { + // this is a problem! we must configure the system first via gradle! + throw java.lang.RuntimeException("Unable to continue! Error reading token!" + + "You must FIRST compile the vaadin resources for DEV or PRODUCTION mode!") + } + + devMode = !tokenJson.getBoolean(InitParameters.SERVLET_PARAMETER_PRODUCTION_MODE) + pNpmEnabled = tokenJson.getBoolean(InitParameters.SERVLET_PARAMETER_ENABLE_PNPM) + extractFromJar = getBoolean(tokenJson, EXTRACT_JAR) + forceExtractOverwrite = getBoolean(tokenJson, EXTRACT_JAR_OVERWRITE, false) + + if (devMode && runningAsJar) { + throw RuntimeException("Invalid run configuration. It is not possible to run DEV MODE from a deployed jar.\n" + + "Something is severely wrong!") + } + + // we are ALWAYS running in full Vaadin14 mode + System.setProperty(Constants.VAADIN_PREFIX + InitParameters.SERVLET_PARAMETER_COMPATIBILITY_MODE, "false") + + if (devMode) { + // set the location of our frontend dir + generated dir when in dev mode + System.setProperty(FrontendUtils.PARAM_FRONTEND_DIR, tokenJson.getString(Constants.FRONTEND_TOKEN)) + System.setProperty(FrontendUtils.PARAM_GENERATED_DIR, tokenJson.getString(Constants.GENERATED_TOKEN)) + } + } + + fun getBoolean(tokenJson: JsonObject, tokenName: String, defaultValue: Boolean = true): Boolean { + return if (tokenJson.hasKey(tokenName)) tokenJson.getBoolean(tokenName) else defaultValue + } + + + fun addServletInitParameters(servlet: ServletInfo) { + servlet + .addInitParam("productionMode", (!devMode).toString()) // this is set via the gradle build + + // have to say where our token file lives + .addInitParam(FrontendUtils.PARAM_TOKEN_FILE, tokenFileName) + + // where our stats.json file lives. This loads via classloader, not via a file!! + .addInitParam(InitParameters.SERVLET_PARAMETER_STATISTICS_JSON, "VAADIN/config/stats.json") + } +} diff --git a/src/dorkbox/vaadin/util/ahoCorasick/DoubleArrayTrie.kt b/src/dorkbox/vaadin/util/ahoCorasick/DoubleArrayTrie.kt index c286668..6151d39 100644 --- a/src/dorkbox/vaadin/util/ahoCorasick/DoubleArrayTrie.kt +++ b/src/dorkbox/vaadin/util/ahoCorasick/DoubleArrayTrie.kt @@ -803,7 +803,7 @@ class DoubleArrayTrie(map: Map? = null, * construct failure table */ private fun constructFailureStates() { - fail = IntArray(size + 1) + fail = IntArray(Math.max(size + 1, 2)) fail[1] = base[0] output = arrayOfNulls(size + 1) @@ -868,7 +868,10 @@ class DoubleArrayTrie(map: Map? = null, val siblings = ArrayList>(initialCapacity) fetch(rootNode, siblings) - insert(siblings) + + if (siblings.isNotEmpty()) { + insert(siblings) + } } /** @@ -1002,11 +1005,28 @@ class DoubleArrayTrie(map: Map? = null, @JvmStatic fun main(args: Array) { - val map = TreeMap() - val keyArray = arrayOf("bmw.com", "cnn.com", "google.com", "reddit.com") + // test outliers + test(hashMapOf()) + + test(hashMapOf("bmw" to "bmw")) + + + var map = hashMapOf() + var keyArray = arrayOf("bmw.com", "cnn.com", "google.com", "reddit.com") for (key in keyArray) { map[key] = key } + test(map) + + map = hashMapOf() + keyArray = arrayOf("bmw.com", "cnn.com", "google.com", "reddit.com", "reddit.google.com") + for (key in keyArray) { + map[key] = key + } + test(map) + } + + fun test(map: Map) { val trie = DoubleArrayTrie(map) val text = "reddit.google.com"