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}