universe@151: /* universe@180: * Copyright 2021 Mike Becker. All rights reserved. universe@151: * universe@151: * Redistribution and use in source and binary forms, with or without universe@151: * modification, are permitted provided that the following conditions are met: universe@151: * universe@151: * 1. Redistributions of source code must retain the above copyright universe@151: * notice, this list of conditions and the following disclaimer. universe@151: * universe@151: * 2. Redistributions in binary form must reproduce the above copyright universe@151: * notice, this list of conditions and the following disclaimer in the universe@151: * documentation and/or other materials provided with the distribution. universe@151: * universe@151: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" universe@151: * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE universe@151: * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE universe@151: * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE universe@151: * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL universe@151: * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR universe@151: * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER universe@151: * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, universe@151: * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE universe@151: * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. universe@151: */ universe@151: universe@151: package de.uapcore.lightpit universe@151: universe@151: import java.sql.SQLException universe@151: import javax.naming.Context universe@151: import javax.naming.InitialContext universe@151: import javax.naming.NamingException universe@151: import javax.servlet.ServletContextEvent universe@151: import javax.servlet.ServletContextListener universe@151: import javax.servlet.annotation.WebListener universe@151: import javax.sql.DataSource universe@151: universe@151: /** universe@151: * Provides access to the database. universe@151: */ universe@151: @WebListener universe@151: class DataSourceProvider : ServletContextListener, LoggingTrait { universe@151: universe@158: enum class Dialect { universe@158: Postgres universe@158: } universe@158: universe@151: /** universe@151: * The database dialect to use. universe@151: * May be overridden by context parameter. universe@151: * universe@151: * @see Constants.CTX_ATTR_DB_DIALECT universe@151: */ universe@158: var dialect = Dialect.Postgres; private set universe@151: universe@151: /** universe@151: * The data source, if available. universe@151: */ universe@151: var dataSource: DataSource? = null universe@151: universe@151: companion object { universe@151: /** universe@151: * The attribute name in the Servlet context under which an instance of this class can be found. universe@151: */ universe@184: const val SC_ATTR_NAME = "lightpit.service.DataSourceProvider" universe@151: universe@151: /** universe@151: * Timeout in seconds for the validation test. universe@151: */ universe@184: private const val DB_TEST_TIMEOUT = 10 universe@151: universe@151: /** universe@151: * The default schema to test against when validating the connection. universe@151: * May be overridden by context parameter. universe@151: * universe@151: * @see Constants.CTX_ATTR_DB_SCHEMA universe@151: */ universe@184: private const val DB_DEFAULT_SCHEMA = "lightpit" universe@151: universe@151: /** universe@151: * The JNDI resource name for the data source. universe@151: */ universe@184: private const val DS_JNDI_NAME = "jdbc/lightpit/app" universe@151: } universe@151: universe@151: private fun checkConnection(ds: DataSource, testSchema: String) { universe@151: try { universe@151: ds.connection.use { conn -> universe@151: if (!conn.isValid(DB_TEST_TIMEOUT)) { universe@151: throw SQLException("Validation check failed.") universe@151: } universe@151: if (conn.isReadOnly) { universe@151: throw SQLException("Connection is read-only and thus unusable.") universe@151: } universe@151: if (conn.schema != testSchema) { universe@151: throw SQLException( universe@151: String.format( universe@151: "Connection is not configured to use the schema %s.", universe@151: testSchema universe@151: ) universe@151: ) universe@151: } universe@151: val metaData = conn.metaData universe@151: logger().info( universe@151: "Connections as {} to {}/{} ready to go.", universe@151: metaData.userName, universe@151: metaData.url, universe@151: conn.schema universe@151: ) universe@151: } universe@151: } catch (ex: SQLException) { universe@151: logger().error("Checking database connection failed", ex) universe@151: } universe@151: } universe@151: universe@151: private fun retrieveDataSource(ctx: Context): DataSource? { universe@151: return try { universe@151: val ret = ctx.lookup(DS_JNDI_NAME) as DataSource universe@151: logger().info("Data source retrieved.") universe@151: ret universe@151: } catch (ex: NamingException) { universe@151: logger().error("Data source {} not available.", DS_JNDI_NAME) universe@151: logger().error("Reason for the missing data source: ", ex) universe@151: null universe@151: } universe@151: } universe@151: universe@151: override fun contextInitialized(sce: ServletContextEvent?) { universe@151: val sc = sce!!.servletContext universe@151: universe@151: val dbSchema = sc.getInitParameter(Constants.CTX_ATTR_DB_SCHEMA) ?: DB_DEFAULT_SCHEMA universe@167: sc.getInitParameter(Constants.CTX_ATTR_DB_DIALECT)?.let { dbDialect -> universe@151: try { universe@158: dialect = Dialect.valueOf(dbDialect) universe@151: } catch (ex: IllegalArgumentException) { universe@151: logger().error( universe@151: "Unknown or unsupported database dialect {}. Defaulting to {}.", universe@151: dbDialect, universe@151: dialect universe@151: ) universe@151: } universe@151: } universe@151: universe@151: dataSource = try { universe@151: logger().debug("Trying to access JNDI context ...") universe@151: val initialCtx: Context = InitialContext() universe@151: val ctx = initialCtx.lookup("java:comp/env") as Context universe@151: retrieveDataSource(ctx) universe@151: } catch (ex: NamingException) { universe@151: logger().error("Cannot access JNDI resources.", ex) universe@151: null universe@151: } catch (ex: ClassCastException) { universe@151: logger().error("Cannot access JNDI resources.", ex) universe@151: null universe@151: } universe@151: universe@151: dataSource?.let { checkConnection(it, dbSchema) } universe@151: universe@151: sc.setAttribute(SC_ATTR_NAME, this) universe@151: logger().info("Database facade injected into ServletContext.") universe@151: } universe@151: universe@151: override fun contextDestroyed(sce: ServletContextEvent?) { universe@151: dataSource = null universe@151: } universe@151: }