1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/src/main/java/de/uapcore/lightpit/DatabaseFacade.java Sat May 09 14:26:31 2020 +0200 1.3 @@ -0,0 +1,178 @@ 1.4 +/* 1.5 + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. 1.6 + * 1.7 + * Copyright 2018 Mike Becker. All rights reserved. 1.8 + * 1.9 + * Redistribution and use in source and binary forms, with or without 1.10 + * modification, are permitted provided that the following conditions are met: 1.11 + * 1.12 + * 1. Redistributions of source code must retain the above copyright 1.13 + * notice, this list of conditions and the following disclaimer. 1.14 + * 1.15 + * 2. Redistributions in binary form must reproduce the above copyright 1.16 + * notice, this list of conditions and the following disclaimer in the 1.17 + * documentation and/or other materials provided with the distribution. 1.18 + * 1.19 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 1.20 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 1.21 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 1.22 + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 1.23 + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 1.24 + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 1.25 + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 1.26 + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 1.27 + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 1.28 + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 1.29 + * POSSIBILITY OF SUCH DAMAGE. 1.30 + * 1.31 + */ 1.32 +package de.uapcore.lightpit; 1.33 + 1.34 +import java.sql.Connection; 1.35 +import java.sql.DatabaseMetaData; 1.36 +import java.sql.SQLException; 1.37 +import java.util.Optional; 1.38 +import javax.naming.Context; 1.39 +import javax.naming.InitialContext; 1.40 +import javax.naming.NamingException; 1.41 +import javax.servlet.ServletContext; 1.42 +import javax.servlet.ServletContextEvent; 1.43 +import javax.servlet.ServletContextListener; 1.44 +import javax.servlet.annotation.WebListener; 1.45 +import javax.sql.DataSource; 1.46 +import org.slf4j.Logger; 1.47 +import org.slf4j.LoggerFactory; 1.48 + 1.49 +/** 1.50 + * Provides access to different privilege layers within the database. 1.51 + */ 1.52 +@WebListener 1.53 +public final class DatabaseFacade implements ServletContextListener { 1.54 + 1.55 + private static final Logger LOG = LoggerFactory.getLogger(DatabaseFacade.class); 1.56 + 1.57 + /** 1.58 + * Timeout in seconds for the validation test. 1.59 + */ 1.60 + private static final int DB_TEST_TIMEOUT = 10; 1.61 + 1.62 + public static enum Dialect { 1.63 + Postgres; 1.64 + } 1.65 + 1.66 + /** 1.67 + * The database dialect to use. 1.68 + * 1.69 + * May be override by context parameter. 1.70 + * 1.71 + * @see Constants#CTX_ATTR_DB_DIALECT 1.72 + */ 1.73 + private Dialect dialect = Dialect.Postgres; 1.74 + 1.75 + /** 1.76 + * The default schema to test against when validating the connection. 1.77 + * 1.78 + * May be overridden by context parameter. 1.79 + * 1.80 + * @see Constants#CTX_ATTR_DB_SCHEMA 1.81 + */ 1.82 + private static final String DB_DEFAULT_SCHEMA = "lightpit"; 1.83 + 1.84 + /** 1.85 + * The attribute name in the Servlet context under which an instance of this class can be found. 1.86 + */ 1.87 + public static final String SC_ATTR_NAME = DatabaseFacade.class.getName(); 1.88 + private ServletContext sc; 1.89 + 1.90 + private static final String DS_JNDI_NAME = "jdbc/lightpit/app"; 1.91 + private Optional<DataSource> dataSource; 1.92 + 1.93 + /** 1.94 + * Returns the data source. 1.95 + * 1.96 + * The Optional returned should never be empty. However, if something goes 1.97 + * wrong during initialization, the data source might be absent. 1.98 + * Hence, users of this data source are forced to check the existence. 1.99 + * 1.100 + * @return a data source 1.101 + */ 1.102 + public Optional<DataSource> getDataSource() { 1.103 + return dataSource; 1.104 + } 1.105 + 1.106 + public Dialect getSQLDialect() { 1.107 + return dialect; 1.108 + } 1.109 + 1.110 + private static void checkConnection(DataSource ds, String testSchema, String errMsg) { 1.111 + try (Connection conn = ds.getConnection()) { 1.112 + if (!conn.isValid(DB_TEST_TIMEOUT)) { 1.113 + throw new SQLException("Validation check failed."); 1.114 + } 1.115 + if (conn.isReadOnly()) { 1.116 + throw new SQLException("Connection is read-only and thus unusable."); 1.117 + } 1.118 + if (!conn.getSchema().equals(testSchema)) { 1.119 + throw new SQLException(String.format("Connection is not configured to use the schema %s.", testSchema)); 1.120 + } 1.121 + DatabaseMetaData metaData = conn.getMetaData(); 1.122 + LOG.info("Connections as {} to {}/{} ready to go.", metaData.getUserName(), metaData.getURL(), conn.getSchema()); 1.123 + } catch (SQLException ex) { 1.124 + LOG.error(errMsg, ex); 1.125 + } 1.126 + } 1.127 + 1.128 + private static Optional<DataSource> retrieveDataSource(Context ctx) { 1.129 + DataSource ret = null; 1.130 + try { 1.131 + ret = (DataSource)ctx.lookup(DS_JNDI_NAME); 1.132 + LOG.info("Data source retrieved."); 1.133 + } catch (NamingException ex) { 1.134 + LOG.error("Data source {} not available.", DS_JNDI_NAME); 1.135 + LOG.error("Reason for the missing data source: ", ex); 1.136 + } 1.137 + return Optional.ofNullable(ret); 1.138 + } 1.139 + 1.140 + @Override 1.141 + public void contextInitialized(ServletContextEvent sce) { 1.142 + sc = sce.getServletContext(); 1.143 + 1.144 + dataSource = null; 1.145 + 1.146 + final String contextName = Optional 1.147 + .ofNullable(sc.getInitParameter(Constants.CTX_ATTR_JNDI_CONTEXT)) 1.148 + .orElse("java:comp/env"); 1.149 + final String dbSchema = Optional 1.150 + .ofNullable(sc.getInitParameter(Constants.CTX_ATTR_DB_SCHEMA)) 1.151 + .orElse(DB_DEFAULT_SCHEMA); 1.152 + final String dbDialect = sc.getInitParameter(Constants.CTX_ATTR_DB_DIALECT); 1.153 + if (dbDialect != null) { 1.154 + try { 1.155 + dialect = Dialect.valueOf(dbDialect); 1.156 + } catch (IllegalArgumentException ex) { 1.157 + LOG.error("Unknown or unsupported database dialect {}. Defaulting to {}.", dbDialect, dialect); 1.158 + } 1.159 + } 1.160 + 1.161 + try { 1.162 + LOG.debug("Trying to access JNDI context {}...", contextName); 1.163 + Context initialCtx = new InitialContext(); 1.164 + Context ctx = (Context) initialCtx.lookup(contextName); 1.165 + 1.166 + dataSource = retrieveDataSource(ctx); 1.167 + 1.168 + dataSource.ifPresent((ds) -> checkConnection(ds, dbSchema, "Checking database connection failed")); 1.169 + } catch (NamingException | ClassCastException ex) { 1.170 + LOG.error("Cannot access JNDI resources.", ex); 1.171 + } 1.172 + 1.173 + sc.setAttribute(SC_ATTR_NAME, this); 1.174 + LOG.info("Database facade injected into ServletContext."); 1.175 + } 1.176 + 1.177 + @Override 1.178 + public void contextDestroyed(ServletContextEvent sce) { 1.179 + dataSource = null; 1.180 + } 1.181 +}