diff -r cfc3d11884ad -r 27a0fdd7bca7 src/main/java/de/uapcore/lightpit/DatabaseFacade.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/java/de/uapcore/lightpit/DatabaseFacade.java Sat May 09 14:26:31 2020 +0200 @@ -0,0 +1,178 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2018 Mike Becker. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +package de.uapcore.lightpit; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.util.Optional; +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.servlet.ServletContext; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import javax.servlet.annotation.WebListener; +import javax.sql.DataSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provides access to different privilege layers within the database. + */ +@WebListener +public final class DatabaseFacade implements ServletContextListener { + + private static final Logger LOG = LoggerFactory.getLogger(DatabaseFacade.class); + + /** + * Timeout in seconds for the validation test. + */ + private static final int DB_TEST_TIMEOUT = 10; + + public static enum Dialect { + Postgres; + } + + /** + * The database dialect to use. + * + * May be override by context parameter. + * + * @see Constants#CTX_ATTR_DB_DIALECT + */ + private Dialect dialect = Dialect.Postgres; + + /** + * The default schema to test against when validating the connection. + * + * May be overridden by context parameter. + * + * @see Constants#CTX_ATTR_DB_SCHEMA + */ + private static final String DB_DEFAULT_SCHEMA = "lightpit"; + + /** + * The attribute name in the Servlet context under which an instance of this class can be found. + */ + public static final String SC_ATTR_NAME = DatabaseFacade.class.getName(); + private ServletContext sc; + + private static final String DS_JNDI_NAME = "jdbc/lightpit/app"; + private Optional dataSource; + + /** + * Returns the data source. + * + * The Optional returned should never be empty. However, if something goes + * wrong during initialization, the data source might be absent. + * Hence, users of this data source are forced to check the existence. + * + * @return a data source + */ + public Optional getDataSource() { + return dataSource; + } + + public Dialect getSQLDialect() { + return dialect; + } + + private static void checkConnection(DataSource ds, String testSchema, String errMsg) { + try (Connection conn = ds.getConnection()) { + if (!conn.isValid(DB_TEST_TIMEOUT)) { + throw new SQLException("Validation check failed."); + } + if (conn.isReadOnly()) { + throw new SQLException("Connection is read-only and thus unusable."); + } + if (!conn.getSchema().equals(testSchema)) { + throw new SQLException(String.format("Connection is not configured to use the schema %s.", testSchema)); + } + DatabaseMetaData metaData = conn.getMetaData(); + LOG.info("Connections as {} to {}/{} ready to go.", metaData.getUserName(), metaData.getURL(), conn.getSchema()); + } catch (SQLException ex) { + LOG.error(errMsg, ex); + } + } + + private static Optional retrieveDataSource(Context ctx) { + DataSource ret = null; + try { + ret = (DataSource)ctx.lookup(DS_JNDI_NAME); + LOG.info("Data source retrieved."); + } catch (NamingException ex) { + LOG.error("Data source {} not available.", DS_JNDI_NAME); + LOG.error("Reason for the missing data source: ", ex); + } + return Optional.ofNullable(ret); + } + + @Override + public void contextInitialized(ServletContextEvent sce) { + sc = sce.getServletContext(); + + dataSource = null; + + final String contextName = Optional + .ofNullable(sc.getInitParameter(Constants.CTX_ATTR_JNDI_CONTEXT)) + .orElse("java:comp/env"); + final String dbSchema = Optional + .ofNullable(sc.getInitParameter(Constants.CTX_ATTR_DB_SCHEMA)) + .orElse(DB_DEFAULT_SCHEMA); + final String dbDialect = sc.getInitParameter(Constants.CTX_ATTR_DB_DIALECT); + if (dbDialect != null) { + try { + dialect = Dialect.valueOf(dbDialect); + } catch (IllegalArgumentException ex) { + LOG.error("Unknown or unsupported database dialect {}. Defaulting to {}.", dbDialect, dialect); + } + } + + try { + LOG.debug("Trying to access JNDI context {}...", contextName); + Context initialCtx = new InitialContext(); + Context ctx = (Context) initialCtx.lookup(contextName); + + dataSource = retrieveDataSource(ctx); + + dataSource.ifPresent((ds) -> checkConnection(ds, dbSchema, "Checking database connection failed")); + } catch (NamingException | ClassCastException ex) { + LOG.error("Cannot access JNDI resources.", ex); + } + + sc.setAttribute(SC_ATTR_NAME, this); + LOG.info("Database facade injected into ServletContext."); + } + + @Override + public void contextDestroyed(ServletContextEvent sce) { + dataSource = null; + } +}