001// --------------------------------------------------------------------------------
002// Copyright 2002-2024 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.sql.ResultSet;
028import java.sql.SQLException;
029import java.util.ArrayList;
030import java.util.List;
031
032public abstract class BaseDatabaseQuery<R extends BaseDatabaseResult> {
033    
034    protected final String sql;
035    protected final EntityPermission entityPermission;
036    
037    protected final Class<R> baseDatabaseResultClass;
038    
039    List<DatabaseResultMethod> databaseResultMethods;
040    
041    @SuppressWarnings("unchecked")
042    protected BaseDatabaseQuery(final String sql, final EntityPermission entityPermission) {
043        this.sql = sql;
044        this.entityPermission = entityPermission;
045        
046        this.baseDatabaseResultClass = (Class<R>)((ParameterizedType)getClass().getGenericSuperclass()).getActualTypeArguments()[0];
047    }
048    
049    private DatabaseResultMethod getDatabaseResultMethod(final String columnLabel) {
050        DatabaseResultMethod databaseResultMethod;
051
052        try {
053            final var getMethod = baseDatabaseResultClass.getMethod("get" + columnLabel);
054            final var returnType = getMethod.getReturnType();
055            final var setMethod = baseDatabaseResultClass.getMethod("set" + columnLabel, returnType);
056            Object factoryInstance = null;
057            Constructor<?> pkConstructor = null;
058            Method getEntityFromPkMethod = null;
059            
060            if(!returnType.equals(Long.class) && !returnType.equals(Integer.class) && !returnType.equals(String.class)) {
061                final var superclass = returnType.getSuperclass();
062
063                if(superclass != null) {
064                    if(superclass.equals(BasePK.class)) {
065                        pkConstructor = returnType.getDeclaredConstructor(Long.class);
066                    } else if(superclass.equals(BaseEntity.class)) {
067                        final var name = returnType.getName();
068                        final var nameComponents = Splitter.on('.').trimResults().omitEmptyStrings().splitToList(name).toArray(new String[0]);
069                        String factoryName = null;
070                        String pkName = null;
071
072                        for(int i = 0 ; i < nameComponents.length ; i++) {
073                            if(nameComponents[i].equals("server")) {
074                                int j = i;
075                                String baseClassName = nameComponents[i + 2];
076
077                                nameComponents[++i] = "factory";
078                                nameComponents[++i] = baseClassName + "Factory";
079                                factoryName = Joiner.on(".").join(nameComponents);
080
081                                nameComponents[j++] = "common";
082                                nameComponents[j++] = "pk";
083                                nameComponents[j] = baseClassName + "PK";
084                                pkName = Joiner.on(".").join(nameComponents);
085
086                                break;
087                            }
088                        }
089
090                        try {
091                            final var classLoader = returnType.getClassLoader();
092                            final var pkType = classLoader.loadClass(pkName);
093                            final var factoryType = classLoader.loadClass(factoryName);
094                            final var getInstanceMethod = factoryType.getDeclaredMethod("getInstance");
095
096                            factoryInstance = getInstanceMethod.invoke(null);
097                            pkConstructor = pkType.getDeclaredConstructor(Long.class);
098                            getEntityFromPkMethod = factoryType.getDeclaredMethod("getEntityFromPK", Session.class, EntityPermission.class, pkType);
099                        } catch(ClassNotFoundException | IllegalAccessException | InvocationTargetException ex) {
100                            throw new PersistenceDatabaseException(ex);
101                        }
102                    }
103                }
104
105                if(pkConstructor == null) {
106                    throw new PersistenceDatabaseException("unsupported Class in getDatabaseResultMethod, " + returnType);
107                }
108            }
109
110            databaseResultMethod = new DatabaseResultMethod(setMethod, returnType, factoryInstance, pkConstructor, getEntityFromPkMethod);
111        } catch(NoSuchMethodException nsme) {
112            throw new PersistenceDatabaseException(nsme);
113        }
114
115        return databaseResultMethod;
116    }
117
118    private List<DatabaseResultMethod> getDatabaseResultMethods(final ResultSet rs) {
119        if(databaseResultMethods == null) {
120            try {
121                final var rsmd = rs.getMetaData();
122
123                databaseResultMethods = new ArrayList<>();
124                for(int columnIndex = 1; columnIndex <= rsmd.getColumnCount(); columnIndex++) {
125                    databaseResultMethods.add(getDatabaseResultMethod(rsmd.getColumnLabel(columnIndex)));
126                }
127            } catch(SQLException se) {
128                throw new PersistenceDatabaseException(se);
129            }
130        }
131
132        return databaseResultMethods;
133    }
134    
135    protected List<R> execute(final Object... params) {
136        final var results = new ArrayList<R>();
137        final var session = ThreadSession.currentSession();
138        final var ps = session.prepareStatement(sql);
139
140        Session.setQueryParams(ps, params);
141
142        try {
143            ps.execute();
144            
145            try(final var rs = ps.getResultSet()) {
146                while(rs.next()) {
147                    int columnIndex = 0;
148
149                    try {
150                        final var baseDatabaseResult = baseDatabaseResultClass.getDeclaredConstructor().newInstance();
151
152                        for(DatabaseResultMethod databaseResultMethod : getDatabaseResultMethods(rs)) {
153                            final var type = databaseResultMethod.type;
154                            Object param = null;
155
156                            columnIndex++;
157                            if(type.equals(Long.class)) {
158                                param = rs.getLong(columnIndex);
159                            } else if(type.equals(Integer.class)) {
160                                param = rs.getInt(columnIndex);
161                            } else if(type.equals(String.class)) {
162                                param = rs.getString(columnIndex);
163                            } else {
164                                final var superclass = type.getSuperclass();
165
166                                if(superclass != null) {
167                                    if(superclass.equals(BasePK.class)) {
168                                        param = databaseResultMethod.pkConstructor.newInstance(rs.getLong(columnIndex));
169                                    } else if(superclass.equals(BaseEntity.class)) {
170                                        Object pk = databaseResultMethod.pkConstructor.newInstance(rs.getLong(columnIndex));
171
172                                        param = databaseResultMethod.getEntityFromPkMethod.invoke(databaseResultMethod.factoryInstance, session, entityPermission, pk);
173                                    }
174                                }
175
176                                if(param == null) {
177                                    throw new PersistenceDatabaseException("unsupported Class in execute, " + type);
178                                }
179                            }
180
181                            databaseResultMethod.setMethod.invoke(baseDatabaseResult, param);
182                        }
183
184                        results.add(baseDatabaseResult);
185                    } catch (NoSuchMethodException | InstantiationException | IllegalAccessException |
186                             InvocationTargetException ex) {
187                        throw new PersistenceDatabaseException(ex);
188                    }
189                }
190            }
191        } catch (SQLException se) {
192            throw new PersistenceDatabaseException(se);
193        }
194        
195        return results;
196    }
197    
198}