|
1 /* |
|
2 * Copyright 2020 Mike Becker. All rights reserved. |
|
3 * |
|
4 * Redistribution and use in source and binary forms, with or without |
|
5 * modification, are permitted provided that the following conditions are met: |
|
6 * |
|
7 * 1. Redistributions of source code must retain the above copyright |
|
8 * notice, this list of conditions and the following disclaimer. |
|
9 * |
|
10 * 2. Redistributions in binary form must reproduce the above copyright |
|
11 * notice, this list of conditions and the following disclaimer in the |
|
12 * documentation and/or other materials provided with the distribution. |
|
13 * |
|
14 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
|
15 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
|
16 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
|
17 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE |
|
18 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
|
19 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
|
20 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
|
21 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
|
22 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
|
23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|
24 */ |
|
25 |
|
26 package de.uapcore.lightpit |
|
27 |
|
28 import java.sql.SQLException |
|
29 import javax.naming.Context |
|
30 import javax.naming.InitialContext |
|
31 import javax.naming.NamingException |
|
32 import javax.servlet.ServletContextEvent |
|
33 import javax.servlet.ServletContextListener |
|
34 import javax.servlet.annotation.WebListener |
|
35 import javax.sql.DataSource |
|
36 |
|
37 enum class DatabaseDialect { |
|
38 Postgres |
|
39 } |
|
40 |
|
41 /** |
|
42 * Provides access to the database. |
|
43 */ |
|
44 @WebListener |
|
45 class DataSourceProvider : ServletContextListener, LoggingTrait { |
|
46 |
|
47 /** |
|
48 * The database dialect to use. |
|
49 * May be overridden by context parameter. |
|
50 * |
|
51 * @see Constants.CTX_ATTR_DB_DIALECT |
|
52 */ |
|
53 var dialect = DatabaseDialect.Postgres; private set |
|
54 |
|
55 /** |
|
56 * The data source, if available. |
|
57 */ |
|
58 var dataSource: DataSource? = null |
|
59 |
|
60 companion object { |
|
61 /** |
|
62 * The attribute name in the Servlet context under which an instance of this class can be found. |
|
63 */ |
|
64 val SC_ATTR_NAME = "lightpit.service.DataSourceProvider" |
|
65 |
|
66 /** |
|
67 * Timeout in seconds for the validation test. |
|
68 */ |
|
69 private val DB_TEST_TIMEOUT = 10 |
|
70 |
|
71 /** |
|
72 * The default schema to test against when validating the connection. |
|
73 * May be overridden by context parameter. |
|
74 * |
|
75 * @see Constants.CTX_ATTR_DB_SCHEMA |
|
76 */ |
|
77 private val DB_DEFAULT_SCHEMA = "lightpit" |
|
78 |
|
79 /** |
|
80 * The JNDI resource name for the data source. |
|
81 */ |
|
82 private val DS_JNDI_NAME = "jdbc/lightpit/app" |
|
83 } |
|
84 |
|
85 private fun checkConnection(ds: DataSource, testSchema: String) { |
|
86 try { |
|
87 ds.connection.use { conn -> |
|
88 if (!conn.isValid(DB_TEST_TIMEOUT)) { |
|
89 throw SQLException("Validation check failed.") |
|
90 } |
|
91 if (conn.isReadOnly) { |
|
92 throw SQLException("Connection is read-only and thus unusable.") |
|
93 } |
|
94 if (conn.schema != testSchema) { |
|
95 throw SQLException( |
|
96 String.format( |
|
97 "Connection is not configured to use the schema %s.", |
|
98 testSchema |
|
99 ) |
|
100 ) |
|
101 } |
|
102 val metaData = conn.metaData |
|
103 logger().info( |
|
104 "Connections as {} to {}/{} ready to go.", |
|
105 metaData.userName, |
|
106 metaData.url, |
|
107 conn.schema |
|
108 ) |
|
109 } |
|
110 } catch (ex: SQLException) { |
|
111 logger().error("Checking database connection failed", ex) |
|
112 } |
|
113 } |
|
114 |
|
115 private fun retrieveDataSource(ctx: Context): DataSource? { |
|
116 return try { |
|
117 val ret = ctx.lookup(DS_JNDI_NAME) as DataSource |
|
118 logger().info("Data source retrieved.") |
|
119 ret |
|
120 } catch (ex: NamingException) { |
|
121 logger().error("Data source {} not available.", DS_JNDI_NAME) |
|
122 logger().error("Reason for the missing data source: ", ex) |
|
123 null |
|
124 } |
|
125 } |
|
126 |
|
127 override fun contextInitialized(sce: ServletContextEvent?) { |
|
128 val sc = sce!!.servletContext |
|
129 |
|
130 val dbSchema = sc.getInitParameter(Constants.CTX_ATTR_DB_SCHEMA) ?: DB_DEFAULT_SCHEMA |
|
131 sc.getInitParameter(Constants.CTX_ATTR_DB_DIALECT)?.let {dbDialect -> |
|
132 try { |
|
133 dialect = DatabaseDialect.valueOf(dbDialect) |
|
134 } catch (ex: IllegalArgumentException) { |
|
135 logger().error( |
|
136 "Unknown or unsupported database dialect {}. Defaulting to {}.", |
|
137 dbDialect, |
|
138 dialect |
|
139 ) |
|
140 } |
|
141 } |
|
142 |
|
143 dataSource = try { |
|
144 logger().debug("Trying to access JNDI context ...") |
|
145 val initialCtx: Context = InitialContext() |
|
146 val ctx = initialCtx.lookup("java:comp/env") as Context |
|
147 retrieveDataSource(ctx) |
|
148 } catch (ex: NamingException) { |
|
149 logger().error("Cannot access JNDI resources.", ex) |
|
150 null |
|
151 } catch (ex: ClassCastException) { |
|
152 logger().error("Cannot access JNDI resources.", ex) |
|
153 null |
|
154 } |
|
155 |
|
156 dataSource?.let { checkConnection(it, dbSchema) } |
|
157 |
|
158 sc.setAttribute(SC_ATTR_NAME, this) |
|
159 logger().info("Database facade injected into ServletContext.") |
|
160 } |
|
161 |
|
162 override fun contextDestroyed(sce: ServletContextEvent?) { |
|
163 dataSource = null |
|
164 } |
|
165 } |