Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ import com.vaadin.flow.component.Key
import com.vaadin.flow.component.KeyModifier
import com.vaadin.flow.component.ShortcutEvent
import com.vaadin.flow.component.Shortcuts
import com.vaadin.flow.component.UI
import com.vaadin.flow.component.contextmenu.MenuItem
import com.vaadin.flow.component.dependency.CssImport
import com.vaadin.flow.component.html.Div
Expand Down Expand Up @@ -104,7 +103,6 @@ class MainWindow(locale: Locale, val logo: String, val href: String, val applica
addLinksListeners()
Shortcuts.addShortcutListener(this, this::goToPreviousPage, Key.PAGE_UP, KeyModifier.of("Alt"))
Shortcuts.addShortcutListener(this, this::goToNextPage, Key.PAGE_DOWN, KeyModifier.of("Alt"))
addBeforeUnloadListener()
instance = this
}

Expand Down Expand Up @@ -243,88 +241,6 @@ class MainWindow(locale: Locale, val logo: String, val href: String, val applica
}
}

/**
* Adds a detatch listener to be excecuted only when a window is closed or navigated away
*/
fun addWindowDetachListener(onDetach: () -> Unit) {
addDetachListener {
isWindowActuallyClosing { isClosing ->
if (isClosing) {
println("The window is closing or navigating away.")
onDetach()
} else {
println("The window was refreshed.")
}
}
}
}

/**
* Determines if the window is actually closing (i.e., the user is closing a tab or
* navigating away) as opposed to simply refreshing the page.
*
* This method uses a `localStorage` flag, `pageReload`, that is set or removed by the
* `addBeforeUnloadListener` method. The logic works as follows:
*
* - If `pageReload` is found and set to `'true'`, it indicates that a page refresh
* occurred. In this case, the method returns `false` to indicate that the window
* is not actually closing.
* - If `pageReload` is not found, it suggests that the window is closing (either
* a tab close or navigation away), so the method returns `true`.
*
* @param callback A lambda function to handle the result: `true` if the window is closing (not a refresh), `false` otherwise.
*/
private fun isWindowActuallyClosing(callback: (Boolean) -> Unit) {
UI.getCurrent().page.executeJs("return localStorage.getItem('pageReload') === 'true';").then {
val isRefresh = it.asBoolean()

callback(!isRefresh)
}
}

/**
* Adds a JavaScript listener for the `beforeunload` event to detect when a page is
* refreshed or when a tab is being closed/navigated away from.
*
* This listener distinguishes between a page refresh (navigation type `1`) and other
* events such as closing the browser tab or navigating away. It uses `localStorage`
* to set a flag based on the detected action, which can be used server-side to infer
* user actions when combined with a Vaadin `DetachListener`.
*
* - On page refresh: A flag `pageReload` is set in the browser's `localStorage`.
* - On tab close or navigation away: The `pageReload` flag is removed from `localStorage`.
*
* Example Usage:
* This function is useful for determining whether to perform cleanup operations (like
* closing a database connection) only on tab close or navigation events but not on a refresh.
*/
private fun addBeforeUnloadListener() {
UI.getCurrent().page.executeJs(
"""
let isUnloading = false;

window.addEventListener('beforeunload', function(event) {
isUnloading = true;
// Save a flag to detect page reload
if (performance.navigation.type === 1) {
// Page refresh detected
localStorage.setItem('pageReload', 'true');
} else {
// Tab close or navigation away
localStorage.removeItem('pageReload');
}
});

// Use 'unload' event as a fallback for closing/navigating away cases
window.addEventListener('unload', function(event) {
if (!isUnloading) {
localStorage.removeItem('pageReload');
}
});
"""
)
}

/**
* Adds a main window listener.
* @param l the listener to be registered.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* Copyright (c) 2013-2022 kopiLeft Services SARL, Tunis TN
* Copyright (c) 1990-2022 kopiRight Managed Solutions GmbH, Wien AT
* Copyright (c) 2013-2025 kopiLeft Services SARL, Tunis TN
* Copyright (c) 1990-2025 kopiRight Managed Solutions GmbH, Wien AT
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
Expand All @@ -18,8 +18,12 @@
package org.kopi.galite.visual.ui.vaadin.visual

import java.sql.SQLException
import java.time.LocalDateTime
import java.util.Date
import java.util.Locale
import java.util.concurrent.CopyOnWriteArrayList

import org.slf4j.LoggerFactory

import com.vaadin.flow.component.AttachEvent
import com.vaadin.flow.component.UI
Expand All @@ -33,6 +37,8 @@ import com.vaadin.flow.router.PreserveOnRefresh
import com.vaadin.flow.server.AppShellRegistry
import com.vaadin.flow.server.AppShellSettings
import com.vaadin.flow.server.ServiceInitEvent
import com.vaadin.flow.server.SessionDestroyEvent
import com.vaadin.flow.server.SessionInitEvent
import com.vaadin.flow.server.VaadinServiceInitListener
import com.vaadin.flow.server.VaadinServlet
import com.vaadin.flow.server.VaadinSession
Expand Down Expand Up @@ -252,10 +258,6 @@ abstract class VApplication(override val registry: Registry) : VerticalLayout(),
mainWindow!!.setBookmarksMenu(DBookmarkMenu(menu!!))
mainWindow!!.setWorkspaceContextItemMenu(DBookmarkMenu(menu!!))
mainWindow!!.connectedUser = userName
mainWindow!!.addWindowDetachListener {
//closing DB connection if the UI is closed after 3 heartbeats
closeConnection()
}
}

fun remove(mainWindow: MainWindow?) {
Expand Down Expand Up @@ -341,6 +343,9 @@ abstract class VApplication(override val registry: Registry) : VerticalLayout(),
if (dBConnection == null) {
throw SQLException(MessageCode.getMessage("VIS-00054"))
} else {
// Add initiated database connection to vaadin session attributes.
// This allows us to clean up / close opened database connections on session destroy event.
addConnectionToSession(dBConnection!!)
// set query trace level
setTraceLevel()
}
Expand Down Expand Up @@ -400,6 +405,31 @@ abstract class VApplication(override val registry: Registry) : VerticalLayout(),
}
}

/**
* Add initiated connection on login to current vaadin session.
* This allows us to clean up / close opened connections on session destroy event.
*
* @param connection Database connection initiated on application login.
*/
fun addConnectionToSession(connection: Connection) {
val session = VaadinSession.getCurrent()

session.lock()
try {
// Get the existing list, or create a new one if null
val connections = session.getAttribute("DB_CONNECTIONS") as? MutableList<Connection>
?: CopyOnWriteArrayList<Connection>().also {
// CopyOnWriteArrayList is used to be thread-safe,
// since multiple tabs/requests may access the list concurrently.
session.setAttribute("DB_CONNECTIONS", it)
}
// Add the new connection to the session attribute.
connections.add(connection)
} finally {
session.unlock()
}
}

/**
* Attaches a window to this application.
* @param window The window to be added.
Expand Down Expand Up @@ -667,6 +697,8 @@ class GaliteAppShellConfigurator: AppShellConfigurator {
}

class ApplicationServiceInitListener: VaadinServiceInitListener {
private val logger = LoggerFactory.getLogger("vaadin.session.lifecycle")

override fun serviceInit(event: ServiceInitEvent) {
val context = event.source.context
val appShellRegistry = AppShellRegistry.getInstance(context)
Expand All @@ -682,5 +714,51 @@ class ApplicationServiceInitListener: VaadinServiceInitListener {
val loadingIndicatorConfiguration = uiInitEvent.ui.loadingIndicatorConfiguration
loadingIndicatorConfiguration.firstDelay = 1000
}

// Add logging when a new vaadin session is initiated.
event.source.addSessionInitListener(::onSessionInit)
// Close all database connections opened within the current vaadin session.
event.source.addSessionDestroyListener(::onSessionDestroy)
}

/**
* Trace the created session ID when a new vaadin session is initiated.
*/
fun onSessionInit(event: SessionInitEvent) {
val session = event.session

logger.info("${LocalDateTime.now()} - New Session [ID : ${session.session.id}] is created.")
}

/**
* Close all database connections opened within the current vaadin session if not already closed.
*/
fun onSessionDestroy(event: SessionDestroyEvent) {
val session = event.session

session.lock()
try {
val connections = session.getAttribute("DB_CONNECTIONS") as? List<Connection>

connections?.forEach { connection ->
try {
// Close all opened connections in the current vaadin session.
if (!connection.poolConnection.isClosed) {
connection.poolConnection.close()
logger.info("${LocalDateTime.now()} - DB connection ${connection.poolConnection} closed " +
"for session [ID : ${session.session.id}].")
}
} catch (ex: Exception) {
logger.warn("${LocalDateTime.now()} - Failed to close connection ${connection.poolConnection} " +
"for session [ID : ${session.session.id}]", ex)
}
}
// Clear the "DB_CONNECTIONS" attribute.
session.setAttribute("DB_CONNECTIONS", null)
// Session destroyed.
logger.info("${LocalDateTime.now()} - Session [ID : ${session.session.id}] is destroyed.")
} finally {
session.unlock()
}
}
}
2 changes: 1 addition & 1 deletion galite-demo/galite-vaadin/src/main/webapp/WEB-INF/web.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,6 @@
<url-pattern>/*</url-pattern>
</servlet-mapping>
<session-config>
<session-timeout>480</session-timeout>
<session-timeout>300</session-timeout>
</session-config>
</web-app>
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
org.gradle.jvmargs=-Xmx4g
#
group=org.kopi
version=1.5.12
version=1.5.12-02TI-SNAPSHOT
Loading