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

This commit is contained in:
Robinson 2021-09-17 17:46:23 +02:00
parent fccb3602ee
commit 837680b077
No known key found for this signature in database
GPG Key ID: 8E7DB78588BD6F5C
8 changed files with 529 additions and 250 deletions

View File

@ -1,15 +1,12 @@
package dorkbox.vaadin 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.VaadinServlet
import com.vaadin.flow.server.frontend.FrontendUtils import com.vaadin.flow.server.frontend.FrontendUtils
import dorkbox.vaadin.devMode.DevModeInitializer import dorkbox.vaadin.devMode.DevModeInitializer
import dorkbox.vaadin.undertow.* import dorkbox.vaadin.undertow.*
import dorkbox.vaadin.util.CallingClass import dorkbox.vaadin.util.CallingClass
import dorkbox.vaadin.util.VaadinConfig
import dorkbox.vaadin.util.ahoCorasick.DoubleArrayTrie import dorkbox.vaadin.util.ahoCorasick.DoubleArrayTrie
import elemental.json.JsonObject
import elemental.json.impl.JsonUtil
import io.github.classgraph.ClassGraph import io.github.classgraph.ClassGraph
import io.github.classgraph.ScanResult import io.github.classgraph.ScanResult
import io.undertow.Undertow 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.CacheHandler
import io.undertow.server.handlers.cache.DirectBufferCache import io.undertow.server.handlers.cache.DirectBufferCache
import io.undertow.server.handlers.resource.CachingResourceManager import io.undertow.server.handlers.resource.CachingResourceManager
import io.undertow.server.handlers.resource.FileResourceManager
import io.undertow.server.handlers.resource.ResourceManager import io.undertow.server.handlers.resource.ResourceManager
import io.undertow.servlet.Servlets import io.undertow.servlet.Servlets
import io.undertow.servlet.api.ServletContainerInitializerInfo import io.undertow.servlet.api.ServletContainerInitializerInfo
@ -51,6 +47,8 @@ import kotlin.reflect.KClass
*/ */
class VaadinApplication() { class VaadinApplication() {
companion object { companion object {
const val debugResources = true
/** /**
* Gets the version number. * Gets the version number.
*/ */
@ -62,15 +60,14 @@ class VaadinApplication() {
} }
} }
val runningAsJar: Boolean
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
val runningAsJar: Boolean
val tempDir: File = File(System.getProperty("java.io.tmpdir", "tmpDir"), "undertow").absoluteFile val tempDir: File = File(System.getProperty("java.io.tmpdir", "tmpDir"), "undertow").absoluteFile
private val onStopList = mutableListOf<Runnable>() private val onStopList = mutableListOf<Runnable>()
val devMode: Boolean val vaadinConfig: VaadinConfig
private val tokenFileName: String
val pNpmEnabled: Boolean
private lateinit var urlClassLoader: URLClassLoader private lateinit var urlClassLoader: URLClassLoader
private lateinit var resourceCollectionManager: ResourceCollectionManager private lateinit var resourceCollectionManager: ResourceCollectionManager
@ -91,62 +88,7 @@ class VaadinApplication() {
runningAsJar = CallingClass.get().getResource("")!!.protocol == "jar" runningAsJar = CallingClass.get().getResource("")!!.protocol == "jar"
// find the config/stats.json to see what mode (PRODUCTION or DEV) we should run in. vaadinConfig = VaadinConfig(runningAsJar, tempDir)
// 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))
}
} }
private fun addAnnotated(annotationScanner: ScanResult, private fun addAnnotated(annotationScanner: ScanResult,
@ -173,12 +115,32 @@ class VaadinApplication() {
} }
} }
private fun recurseAllFiles(allRelativeFilePaths: MutableSet<WebResource>, 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") @Suppress("DuplicatedCode")
fun initResources() { fun initResources() {
val metaInfResources = "META-INF/resources" val metaInfResources = "META-INF/resources"
val buildMetaInfResources = "build/resources/main/META-INF/resources"
val metaInfValLength = metaInfResources.length + 1 val metaInfValLength = metaInfResources.length + 1
val buildMetaInfResources = "build/resources/main/META-INF/resources"
// resource locations are tricky... // resource locations are tricky...
// when a JAR : META-INF/resources // when a JAR : META-INF/resources
@ -206,25 +168,38 @@ class VaadinApplication() {
.scan() .scan()
val jarLocations = mutableSetOf<URL>() val jarLocations = mutableSetOf<WebResourceString>()
val diskLocations = mutableSetOf<URL>() val diskLocations = mutableSetOf<WebResource>()
val urlClassLoader = mutableSetOf<URL>()
if (runningAsJar) { if (runningAsJar && vaadinConfig.extractFromJar) {
// collect all the resources available. // running from JAR (really just temp-dir, since the jar is extracted!)
logger.info { "Extracting all jar $metaInfResources files to $tempDir" } logger.info { "Running from jar and extracting all jar [$metaInfResources] files to [$tempDir]" }
var lastFile: File? = null
scanResultJarDependencies.allResources.forEach { resource -> scanResultJarDependencies.allResources.forEach { resource ->
val resourcePath = resource.pathRelativeToClasspathElement val resourcePath = resource.pathRelativeToClasspathElement
val relativePath = resourcePath.substring(metaInfValLength) val relativePath = resourcePath.substring(metaInfValLength)
logger.trace { // // we only care about VAADIN and frontend resources for JARs -- everything else is compiled as part of the webpack process.
"Discovered resource: $relativePath" // 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 // 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 val parentFile = outputFile.parentFile
if (!parentFile.isDirectory && !parentFile.mkdirs()) { if (!parentFile.isDirectory && !parentFile.mkdirs()) {
logger.error { "Unable to create output directory $parentFile" } logger.error { "Unable to create output directory $parentFile" }
@ -233,148 +208,254 @@ class VaadinApplication() {
outputFile.outputStream().use { input.copyTo(it) } 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) } else if (runningAsJar) {
urlClassLoader = object : URLClassLoader(locations, this.javaClass.classLoader) { // running from JAR (not-extracted!)
override fun getResource(name: String): URL? { logger.info { "Running from jar files" }
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
val fixedName = name.substring("META-INF".length) var lastFile: File? = null
return super.getResource(fixedName) 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<String, String>()
val diskResourceRequestMap = TreeMap<String, URL>()
} else {
// running from IDE
logger.info { "Running from IDE files" }
var lastFile: File? = null
scanResultJarDependencies.allResources.forEach { resource -> scanResultJarDependencies.allResources.forEach { resource ->
val resourcePath = resource.pathRelativeToClasspathElement val resourcePath = resource.pathRelativeToClasspathElement
val relativePath = resourcePath.substring(metaInfValLength) val relativePath = resourcePath.substring(metaInfValLength)
logger.error { "Jar resource: $relativePath" } 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 // jar file this resource is from -- BUT NOT THE RESOURCE ITSELF
// some-of the resources are loaded with a "META-INF" prefix by the vaadin servlet urlClassLoader.add(resource.classpathElementURL)
jarResourceRequestMap["META-INF/$relativePath"] = resourcePath
} }
// some static resources from disk are ALSO loaded by the classloader. // some static resources from disk are ALSO loaded by the classloader.
scanResultLocalDependencies.allResources.forEach { resource -> scanResultLocalDependencies.allResources.forEach { resource ->
val resourcePath = resource.pathRelativeToClasspathElement val resourcePath = resource.pathRelativeToClasspathElement
val relativePath = resourcePath.substring(metaInfValLength) val relativePath = resourcePath.substring(metaInfValLength)
logger.error { "Classpath resource: $relativePath" } logger.error { "Local resource: $relativePath" }
diskLocations.add(resource.classpathElementURL) diskLocations.add(WebResource(relativePath, resource.url))
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
} }
// we also have resources that are OUTSIDE the classpath (ie: in the temp build dir) // 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! // This is necessary BECAUSE we have to be able to ALSO serve resources via the classloader!
val buildDirMetaInfResources = File(buildMetaInfResources).absoluteFile.normalize() val buildDirMetaInfResources = File(buildMetaInfResources).absoluteFile.normalize()
val rootPathLength = buildDirMetaInfResources.toURI().toURL().path.length 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<String, String>()
val jarUrlResourceRequestMap = mutableMapOf<String, URL>()
val diskResourceRequestMap = mutableMapOf<String, URL>()
// so we can use the undertow cache to serve resources, instead of the vaadin servlet (which doesn't cache, and is really slow) jarLocations.forEach { (requestPath, resourcePath, relativeResourcePath) ->
// NOTE: this is what ALSO will load the stats.json file! // make sure the path is WWW request compatible (ie: no spaces/etc)
urlClassLoader = object : URLClassLoader(jarLocations.toTypedArray(), this.javaClass.classLoader) { val wwwCompatiblePath = java.net.URLDecoder.decode(requestPath, Charsets.UTF_8)
val jarTrie = DoubleArrayTrie(jarResourceRequestMap)
val diskTrie = DoubleArrayTrie(diskResourceRequestMap)
override fun getResource(name: String): URL? { // this adds the resource to our request map, used by our trie
// check disk first jarStringResourceRequestMap[wwwCompatiblePath] = relativeResourcePath
val diskResourcePath = diskTrie[name] jarUrlResourceRequestMap[wwwCompatiblePath] = resourcePath
if (diskResourcePath != null) {
return diskResourcePath
}
val jarResourcePath = jarTrie[name] if (!wwwCompatiblePath.startsWith("META-INF")) {
if (jarResourcePath != null) { // some-of the resources are loaded with a "META-INF" prefix by the vaadin servlet
return super.getResource(jarResourcePath) jarStringResourceRequestMap["META-INF/$wwwCompatiblePath"] = relativeResourcePath
} jarUrlResourceRequestMap["META-INF/$wwwCompatiblePath"] = resourcePath
return super.getResource(name)
}
} }
// we also have resources that are OUTSIDE the classpath (ie: in the temp build dir) if (!wwwCompatiblePath.startsWith('/')) {
diskLocations.add(buildDirMetaInfResources.toURI().toURL()) // 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<JarResourceManager>()
// 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 // collect all the resources available from each location to ALSO be handled by undertow
val diskResources = ArrayList<FileResourceManager>() // val diskResources = ArrayList<FileResourceManager>()
val jarResources = ArrayList<JarResourceManager>() // val fileResources = ArrayList<FileResourceManager>()
val fileResources = ArrayList<FileResourceManager>()
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 // diskLocations.forEach { (requestPath, resourcePath) ->
// vaadin will request resources based on THAT location as well. // val wwwCompatiblePath = java.net.URLDecoder.decode(requestPath, Charsets.UTF_8)
val metaInfResourcesLocation = File(file, metaInfResources) // val diskFile = resourcePath.file
if (metaInfResourcesLocation.isDirectory) { //
diskResources.add(FileResourceManager(metaInfResourcesLocation)) //
// // this serves a BASE location!
// we will also serve content from ALL child directories // diskResources.add(FileResourceManager(metaInfResourcesLocation))
metaInfResourcesLocation.listFiles()?.forEach { childFile -> //
when { // // if this location is where our "META-INF/resources" directory exists, ALSO add that location, because the
childFile.isDirectory -> prefixResources.add("/${childFile.relativeTo(metaInfResourcesLocation)}") // // vaadin will request resources based on THAT location as well.
else -> exactResources.add("/${childFile.relativeTo(metaInfResourcesLocation)}") // val metaInfResourcesLocation = File(file, metaInfResources)
} //
} // if (metaInfResourcesLocation.isDirectory) {
} // diskResources.add(FileResourceManager(metaInfResourcesLocation))
//
diskResources.add(FileResourceManager(file)) // // we will also serve content from ALL child directories
// metaInfResourcesLocation.listFiles()?.forEach { childFile ->
// we will also serve content from ALL child directories // val element = "/${childFile.relativeTo(metaInfResourcesLocation)}"
// (except for the META-INF dir, which we are ALREADY serving content) // if (debugResources) {
file.listFiles()?.forEach { childFile -> // println(" DISK: $cleanedUrl")
when { // }
childFile.isDirectory -> { //
if (childFile.name != "META-INF") { // when {
prefixResources.add("/${childFile.relativeTo(file)}") // childFile.isDirectory -> prefixResources.add(element)
} // else -> exactResources.add(element)
} // }
else -> exactResources.add("/${childFile.relativeTo(file)}") // }
} // }
} //
} // 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 // When we are searching for resources, the following search order is optimized for access speed and request hit order
// DISK // DISK
@ -384,62 +465,39 @@ class VaadinApplication() {
// flow-push // flow-push
// flow-server // flow-server
// then every other jar // then every other jar
resources.addAll(diskResources) resources.add(strictFileResourceManager)
resources.addAll(fileResources) 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) { // val client = jarResources.firstOrNull { it.name.contains("flow-client") }
// these jars will ALWAYS be available (as of Vaadin 14.2) // val push = jarResources.firstOrNull { it.name.contains("flow-push") }
// if we are running from a fatjar, then the resources will likely be extracted (so this is not necessary) // val server = jarResources.firstOrNull { it.name.contains("flow-server") }
jarResources.remove(client) //
jarResources.remove(push) // if (client != null && push != null && server != null) {
jarResources.remove(server) // // 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)
resources.add(client) // jarResources.remove(client)
resources.add(push) // jarResources.remove(push)
resources.add(server) // jarResources.remove(server)
} //
// resources.add(client)
resources.addAll(jarResources) // resources.add(push)
// resources.add(server)
// }
//
// resources.addAll(jarResources)
// TODO: Have a 404 resource handler to log when a requested file is not found! // 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/'. // 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/ // 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) // 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<String, URL>,
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, fun initServlet(enableCachedHandlers: Boolean, cacheTimeoutSeconds: Int,
servletClass: Class<out Servlet> = VaadinServlet::class.java, servletClass: Class<out Servlet> = VaadinServlet::class.java,
servletName: String = "Vaadin", servletName: String = "Vaadin",
@ -473,20 +531,15 @@ class VaadinApplication() {
.setLoadOnStartup(1) .setLoadOnStartup(1)
.setAsyncSupported(true) .setAsyncSupported(true)
.setExecutor(null) // we use coroutines! .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. // have to say where our NPM/dev mode files live.
.addInitParam(FrontendUtils.PROJECT_BASEDIR, File("").absolutePath) .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") .addInitParam("enable-websockets", "true")
.addMapping("/*") .addMapping("/*")
vaadinConfig.addServletInitParameters(servlet)
// setup (or change) custom config options ( // setup (or change) custom config options (
servletConfig(servlet) servletConfig(servlet)
@ -536,7 +589,7 @@ class VaadinApplication() {
ClassGraph().enableAnnotationInfo().enableClassInfo().scan().use { annotationScanner -> ClassGraph().enableAnnotationInfo().enableClassInfo().scan().use { annotationScanner ->
for (service in serviceLoader) { for (service in serviceLoader) {
val classSet = hashSetOf<Class<*>>() val classSet: HashSet<Class<*>> = hashSetOf()
val javaClass = service.javaClass val javaClass = service.javaClass
val annotation= javaClass.getAnnotation(HandlesTypes::class.java) val annotation= javaClass.getAnnotation(HandlesTypes::class.java)
@ -549,7 +602,7 @@ class VaadinApplication() {
if (classSet.isNotEmpty()) { if (classSet.isNotEmpty()) {
if (javaClass == com.vaadin.flow.server.startup.DevModeInitializer::class.java) { 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. // instead of the default, we load **OUR** dev-mode initializer.
// The vaadin one is super buggy for custom environments // The vaadin one is super buggy for custom environments
servletBuilder.addServletContainerInitializer( 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()' // 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 // 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 // are hardcoded to "src/main/resources/META-INF/resources/frontend", so we have to
@ -651,7 +704,11 @@ class VaadinApplication() {
} }
fun handleRequest(exchange: HttpServerExchange) { fun handleRequest(exchange: HttpServerExchange) {
// dev-mode : incoming requests USUALLY start with a '/'
val path = exchange.relativePath 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. // 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. // Because this is non-blocking, this is also the preferred way to do this for performance.
@ -743,7 +800,12 @@ class VaadinApplication() {
// servletWebapp.destroy() // servletWebapp.destroy()
// allChannels.close().awaitUninterruptibly() // allChannels.close().awaitUninterruptibly()
worker?.shutdown() // maybe? val worker = worker
if (worker != null) {
worker.shutdown()
worker.awaitTermination(10L, TimeUnit.SECONDS)
}
undertowServer?.stop() undertowServer?.stop()
} finally { } finally {
onStopList.forEach { onStopList.forEach {

View File

@ -0,0 +1,11 @@
package dorkbox.vaadin.compiler
/**
*
*/
object VaadinCompile {
fun print() {
println("vaadin compile from maven")
}
}

View File

@ -15,12 +15,13 @@
*/ */
package dorkbox.vaadin.undertow package dorkbox.vaadin.undertow
import dorkbox.vaadin.VaadinApplication
import dorkbox.vaadin.util.ahoCorasick.DoubleArrayTrie
import io.undertow.UndertowMessages import io.undertow.UndertowMessages
import io.undertow.server.handlers.resource.Resource import io.undertow.server.handlers.resource.Resource
import io.undertow.server.handlers.resource.ResourceChangeListener import io.undertow.server.handlers.resource.ResourceChangeListener
import io.undertow.server.handlers.resource.ResourceManager import io.undertow.server.handlers.resource.ResourceManager
import io.undertow.server.handlers.resource.URLResource import io.undertow.server.handlers.resource.URLResource
import java.io.File
import java.io.IOException import java.io.IOException
import java.net.URL import java.net.URL
@ -31,26 +32,19 @@ import java.net.URL
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Dorkbox LLC * @author Dorkbox LLC
*/ */
internal class JarResourceManager(val jarFile: String, base: String = "") : ResourceManager { internal class JarResourceManager(val name: String, val trie: DoubleArrayTrie<URL>) : 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 -> ""
}
@Throws(IOException::class) @Throws(IOException::class)
override fun getResource(path: String): Resource? { override fun getResource(path: String): Resource? {
val cleanedPath = when { if (VaadinApplication.debugResources) {
path.startsWith("/") -> path println("REQUEST jar: $path")
else -> "/$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) val resource = URLResource(url, path)
if (path.isNotBlank() && path != "/" && resource.contentLength < 0) { if (path.isNotBlank() && path != "/" && resource.contentLength < 0) {
@ -77,6 +71,6 @@ internal class JarResourceManager(val jarFile: String, base: String = "") : Reso
} }
override fun toString(): String { override fun toString(): String {
return "JarResourceManager($jarFile)" return "JarResourceManager($name)"
} }
} }

View File

@ -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<URL>) : 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)"
}
}

View File

@ -0,0 +1,5 @@
package dorkbox.vaadin.undertow
import java.net.URL
data class WebResource(val requestPath: String, val resourcePath: URL)

View File

@ -0,0 +1,5 @@
package dorkbox.vaadin.undertow
import java.net.URL
data class WebResourceString(val requestPath: String, val resourcePath: URL, val relativeResourcePath: String)

View File

@ -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")
}
}

View File

@ -803,7 +803,7 @@ class DoubleArrayTrie<V>(map: Map<String, V>? = null,
* construct failure table * construct failure table
*/ */
private fun constructFailureStates() { private fun constructFailureStates() {
fail = IntArray(size + 1) fail = IntArray(Math.max(size + 1, 2))
fail[1] = base[0] fail[1] = base[0]
output = arrayOfNulls(size + 1) output = arrayOfNulls(size + 1)
@ -868,7 +868,10 @@ class DoubleArrayTrie<V>(map: Map<String, V>? = null,
val siblings = ArrayList<Pair<Int, State>>(initialCapacity) val siblings = ArrayList<Pair<Int, State>>(initialCapacity)
fetch(rootNode, siblings) fetch(rootNode, siblings)
insert(siblings)
if (siblings.isNotEmpty()) {
insert(siblings)
}
} }
/** /**
@ -1002,11 +1005,28 @@ class DoubleArrayTrie<V>(map: Map<String, V>? = null,
@JvmStatic @JvmStatic
fun main(args: Array<String>) { fun main(args: Array<String>) {
val map = TreeMap<String, String>() // test outliers
val keyArray = arrayOf("bmw.com", "cnn.com", "google.com", "reddit.com") test(hashMapOf())
test(hashMapOf("bmw" to "bmw"))
var map = hashMapOf<String, String>()
var keyArray = arrayOf("bmw.com", "cnn.com", "google.com", "reddit.com")
for (key in keyArray) { for (key in keyArray) {
map[key] = key 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<String, String>) {
val trie = DoubleArrayTrie(map) val trie = DoubleArrayTrie(map)
val text = "reddit.google.com" val text = "reddit.google.com"