001// -------------------------------------------------------------------------------- 002// Copyright 2002-2026 Echo Three, LLC 003// 004// Licensed under the Apache License, Version 2.0 (the "License"); 005// you may not use this file except in compliance with the License. 006// You may obtain a copy of the License at 007// 008// http://www.apache.org/licenses/LICENSE-2.0 009// 010// Unless required by applicable law or agreed to in writing, software 011// distributed under the License is distributed on an "AS IS" BASIS, 012// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013// See the License for the specific language governing permissions and 014// limitations under the License. 015// -------------------------------------------------------------------------------- 016 017package com.echothree.util.server.persistence; 018 019import com.echothree.util.common.exception.PersistenceDatabaseException; 020import java.sql.SQLException; 021import javax.naming.InitialContext; 022import javax.naming.NamingException; 023import javax.sql.DataSource; 024import org.jooq.DSLContext; 025import org.jooq.SQLDialect; 026import org.jooq.conf.Settings; 027import org.jooq.impl.DSL; 028import org.jooq.tools.jdbc.JDBCUtils; 029 030public class DslContextFactory { 031 032 private static final String DS = "java:/EchoThreeDS"; 033 private static final String NTDS = "java:/EchoThreeNTDS"; 034 035 // Template for both contexts; must not be modified after initialization. 036 private static final Settings SETTINGS = createSettings(); 037 038 private final DataSource ds; 039 private final DataSource ntds; 040 private final SQLDialect dsDialect; 041 private final SQLDialect ntdsDialect; 042 043 // Shared across requests; configurations and settings must not be modified after initialization. 044 private final DSLContext dslContext; 045 private final DSLContext ntDslContext; 046 047 @SuppressWarnings("BanJNDI") 048 protected DslContextFactory() { 049 try { 050 var jndiContext = new InitialContext(); 051 ds = (DataSource)jndiContext.lookup(DS); 052 ntds = (DataSource)jndiContext.lookup(NTDS); 053 dsDialect = detectDialect(ds); 054 ntdsDialect = detectDialect(ntds); 055 056 if(dsDialect != ntdsDialect) { 057 throw new PersistenceDatabaseException(""" 058 Likely JDBC misconfiguration: %s uses %s but %s uses %s; both DataSources must use the same database dialect 059 """.formatted(DS, dsDialect, NTDS, ntdsDialect)); 060 } 061 062 dslContext = DSL.using(ds, dsDialect, SETTINGS); 063 ntDslContext = DSL.using(ntds, ntdsDialect, SETTINGS); 064 } catch (NamingException ne) { 065 throw new PersistenceDatabaseException(ne); 066 } 067 } 068 069 private static class DslContextFactoryHolder { 070 private static final DslContextFactory instance = new DslContextFactory(); 071 } 072 073 public static DslContextFactory getInstance() { 074 return DslContextFactoryHolder.instance; 075 } 076 077 private SQLDialect detectDialect(DataSource dataSource) { 078 try(var connection = dataSource.getConnection()) { 079 var dialect = JDBCUtils.dialect(connection); 080 081 if(dialect == SQLDialect.DEFAULT) { 082 throw new PersistenceDatabaseException("Unable to determine database dialect"); 083 } 084 085 // List exists in DatabaseMutexFlavorProducer also to support Mutexes. 086 return switch(dialect.family()) { 087 case MYSQL, MARIADB, POSTGRES -> dialect; 088 default -> throw new PersistenceDatabaseException("Unsupported database dialect: " + dialect); 089 }; 090 } catch(SQLException e) { 091 throw new PersistenceDatabaseException(e); 092 } 093 } 094 095 private static Settings createSettings() { 096 // The datasource selects the tenant database. Generated schema names 097 // describe the model and must not qualify runtime SQL. 098 // 099 // PostgreSQL connections must use a search_path that resolves the intended schema. 100 // Disabling schema rendering also disables catalog qualification. 101 return new Settings() 102 .withRenderSchema(false); 103 } 104 105 public DSLContext getDslContext() { 106 return dslContext; 107 } 108 109 public DSLContext getNTDslContext() { 110 return ntDslContext; 111 } 112 113}