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"