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 com.echothree.util.common.persistence.BasePK; 021import com.google.common.base.Joiner; 022import com.google.common.base.Splitter; 023import java.lang.reflect.Constructor; 024import java.lang.reflect.InvocationTargetException; 025import java.lang.reflect.Method; 026import java.lang.reflect.ParameterizedType; 027import java.util.ArrayList; 028import java.util.List; 029import org.jooq.DSLContext; 030import org.jooq.Field; 031import org.jooq.Record; 032import org.jooq.ResultQuery; 033import org.jooq.exception.DataAccessException; 034 035public abstract class BaseDatabaseQuery<R extends BaseDatabaseResult> { 036 037 private final EntityPermission entityPermission; 038 private final Class<R> baseDatabaseResultClass; 039 private final Constructor<R> baseDatabaseResultConstructor; 040 041 @SuppressWarnings("unchecked") 042 protected BaseDatabaseQuery(final EntityPermission entityPermission) { 043 this.entityPermission = entityPermission; 044 045 this.baseDatabaseResultClass = (Class<R>)((ParameterizedType)getClass().getGenericSuperclass()).getActualTypeArguments()[0]; 046 047 try { 048 baseDatabaseResultConstructor = baseDatabaseResultClass.getDeclaredConstructor(); 049 } catch(NoSuchMethodException ex) { 050 throw new PersistenceDatabaseException(ex); 051 } 052 } 053 054 protected DSLContext getDslContext() { 055 return ThreadSession.currentSession().getDslContext(); 056 } 057 058 private PersistenceDatabaseException mappingException(final String columnLabel, final Class<?> type, final Throwable cause) { 059 var exception = new PersistenceDatabaseException("Cannot map column '" + columnLabel + "' to " 060 + baseDatabaseResultClass.getName() + "; expected type: " + (type == null ? "unresolved" : type.getName())); 061 062 exception.initCause(cause instanceof InvocationTargetException invocation ? invocation.getCause() : cause); 063 064 return exception; 065 } 066 067 private DatabaseResultMethod getDatabaseResultMethod(final String columnLabel) { 068 Class<?> returnType = null; 069 070 try { 071 final var getMethod = baseDatabaseResultClass.getMethod("get" + columnLabel); 072 073 returnType = getMethod.getReturnType(); 074 075 final var setMethod = baseDatabaseResultClass.getMethod("set" + columnLabel, returnType); 076 Object factoryInstance = null; 077 Constructor<?> pkConstructor = null; 078 Method getEntityFromPkMethod = null; 079 080 if(!returnType.equals(Long.class) && !returnType.equals(Integer.class) && !returnType.equals(String.class)) { 081 final var superclass = returnType.getSuperclass(); 082 083 if(superclass != null) { 084 if(superclass.equals(BasePK.class)) { 085 pkConstructor = returnType.getDeclaredConstructor(Long.class); 086 } else if(superclass.equals(BaseEntity.class)) { 087 final var name = returnType.getName(); 088 final var nameComponents = Splitter.on('.').trimResults().omitEmptyStrings().splitToList(name).toArray(new String[0]); 089 String factoryName = null; 090 String pkName = null; 091 092 for(var i = 0; i < nameComponents.length ; i++) { 093 if(nameComponents[i].equals("server")) { 094 var j = i; 095 var baseClassName = nameComponents[i + 2]; 096 097 nameComponents[++i] = "factory"; 098 nameComponents[++i] = baseClassName + "Factory"; 099 factoryName = Joiner.on(".").join(nameComponents); 100 101 nameComponents[j++] = "common"; 102 nameComponents[j++] = "pk"; 103 nameComponents[j] = baseClassName + "PK"; 104 pkName = Joiner.on(".").join(nameComponents); 105 106 break; 107 } 108 } 109 110 final var classLoader = returnType.getClassLoader(); 111 final var pkType = classLoader.loadClass(pkName); 112 final var factoryType = classLoader.loadClass(factoryName); 113 final var getInstanceMethod = factoryType.getDeclaredMethod("getInstance"); 114 115 factoryInstance = getInstanceMethod.invoke(null); 116 pkConstructor = pkType.getDeclaredConstructor(Long.class); 117 getEntityFromPkMethod = factoryType.getDeclaredMethod("getEntityFromPK", EntityPermission.class, pkType); 118 } 119 } 120 121 if(pkConstructor == null) { 122 throw new IllegalArgumentException("Unsupported result type: " + returnType.getName()); 123 } 124 } 125 126 return new DatabaseResultMethod(columnLabel, setMethod, returnType, factoryInstance, pkConstructor, getEntityFromPkMethod); 127 } catch(ReflectiveOperationException | RuntimeException ex) { 128 throw mappingException(columnLabel, returnType, ex); 129 } 130 } 131 132 private DatabaseResultMethod[] getDatabaseResultMethods(final Field<?>[] fields) { 133 final var methods = new DatabaseResultMethod[fields.length]; 134 135 for(var columnIndex = 0; columnIndex < fields.length; columnIndex++) { 136 methods[columnIndex] = getDatabaseResultMethod(fields[columnIndex].getName()); 137 } 138 return methods; 139 } 140 141 private Object getDatabaseResultValue(final Record record, final int columnIndex, final DatabaseResultMethod method) 142 throws ReflectiveOperationException { 143 return switch(method.kind) { 144 case SCALAR -> record.get(columnIndex, method.type); 145 case PRIMARY_KEY, ENTITY -> { 146 var key = record.get(columnIndex); 147 if(key == null) { 148 yield null; 149 } 150 if(!(key instanceof BasePK)) { 151 key = method.pkConstructor.newInstance(record.get(columnIndex, Long.class)); 152 } 153 yield method.kind == DatabaseResultMethod.Kind.PRIMARY_KEY ? key 154 : method.getEntityFromPkMethod.invoke(method.factoryInstance, entityPermission, key); 155 } 156 }; 157 } 158 159 protected List<R> execute(final ResultQuery<?> query) { 160 final var results = new ArrayList<R>(); 161 162 try(final var cursor = query.fetchLazy()) { 163 if(cursor.hasNext()) { 164 final var methods = getDatabaseResultMethods(cursor.fields()); 165 166 for(var record : cursor) { 167 final R result; 168 169 try { 170 result = baseDatabaseResultConstructor.newInstance(); 171 } catch(InstantiationException | IllegalAccessException | InvocationTargetException ex) { 172 var exception = new PersistenceDatabaseException("Cannot instantiate result " + baseDatabaseResultClass.getName()); 173 174 exception.initCause(ex instanceof InvocationTargetException invocation ? invocation.getCause() : ex); 175 176 throw exception; 177 } 178 179 for(var columnIndex = 0; columnIndex < methods.length; columnIndex++) { 180 final var method = methods[columnIndex]; 181 182 try { 183 method.setMethod.invoke(result, getDatabaseResultValue(record, columnIndex, method)); 184 } catch(ReflectiveOperationException | RuntimeException ex) { 185 throw mappingException(method.columnLabel, method.type, ex); 186 } 187 } 188 results.add(result); 189 } 190 } 191 } catch(DataAccessException ex) { 192 throw new PersistenceDatabaseException(ex); 193 } 194 195 return results; 196 } 197 198}