Sat, 16 May 2020 15:45:06 +0200
fixes bug where displaying an error page for missing data source would also require that data source (error pages don't try to get database connections now)
also improves error pages in general
/* * 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 org.slf4j.Logger; import org.slf4j.LoggerFactory; 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 java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.SQLException; import java.util.Optional; /** * 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; /** * Specifies the database dialect. */ public enum Dialect { Postgres } /** * The database dialect to use. * <p> * May be overridden by context parameter. * * @see Constants#CTX_ATTR_DB_DIALECT */ private Dialect dialect = Dialect.Postgres; /** * The default schema to test against when validating the connection. * <p> * 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 static final String DS_JNDI_NAME = "jdbc/lightpit/app"; private DataSource dataSource; /** * Returns the data source. * * @return a data source */ public DataSource getDataSource() { return dataSource; } public Dialect getSQLDialect() { return dialect; } private static void checkConnection(DataSource ds, String testSchema) { 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("Checking database connection failed", ex); } } private static DataSource 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 ret; } @Override public void contextInitialized(ServletContextEvent sce) { ServletContext sc = sce.getServletContext(); dataSource = null; 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 ..."); Context initialCtx = new InitialContext(); Context ctx = (Context) initialCtx.lookup("java:comp/env"); dataSource = retrieveDataSource(ctx); if (dataSource != null) { checkConnection(dataSource, dbSchema); } } 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; } }