001/*
002 * Stallion Core: A Modern Web Framework
003 *
004 * Copyright (C) 2015 - 2016 Stallion Software LLC.
005 *
006 * This program is free software: you can redistribute it and/or modify it under the terms of the
007 * GNU General Public License as published by the Free Software Foundation, either version 2 of
008 * the License, or (at your option) any later version. This program is distributed in the hope that
009 * it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
010 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
011 * License for more details. You should have received a copy of the GNU General Public License
012 * along with this program.  If not, see <http://www.gnu.org/licenses/gpl-2.0.html>.
013 *
014 *
015 *
016 */
017
018package io.stallion.dataAccess.db;
019
020import com.mchange.v2.c3p0.ComboPooledDataSource;
021
022import io.stallion.dataAccess.AlternativeKey;
023import io.stallion.dataAccess.DynamicModelDefinition;
024import io.stallion.dataAccess.Tickets;
025import io.stallion.dataAccess.UniqueKey;
026import io.stallion.dataAccess.Model;
027import io.stallion.dataAccess.db.converters.*;
028import io.stallion.dataAccess.db.mysql.MySqlDbImplementation;
029import io.stallion.dataAccess.db.postgres.PostgresDbImplementation;
030import io.stallion.exceptions.ConfigException;
031import io.stallion.exceptions.UsageException;
032import io.stallion.plugins.javascript.BaseJavascriptModel;
033import io.stallion.services.Log;
034import io.stallion.reflection.PropertyUtils;
035import io.stallion.settings.Settings;
036import io.stallion.settings.childSections.DbConfig;
037import io.stallion.utils.Literals;
038import io.stallion.utils.StallionClassLoader;
039import org.apache.commons.codec.digest.DigestUtils;
040import org.apache.commons.dbutils.QueryRunner;
041import org.apache.commons.dbutils.ResultSetHandler;
042import org.apache.commons.dbutils.handlers.BeanListHandler;
043import org.apache.commons.dbutils.handlers.MapHandler;
044import org.apache.commons.dbutils.handlers.MapListHandler;
045import org.apache.commons.dbutils.handlers.ScalarHandler;
046import org.apache.commons.lang3.StringUtils;
047
048
049import javax.persistence.Column;
050import javax.persistence.Table;
051import javax.sql.DataSource;
052import java.beans.PropertyVetoException;
053import java.lang.reflect.Field;
054import java.sql.*;
055import java.lang.reflect.Method;
056import java.time.Instant;
057import java.time.LocalDateTime;
058import java.time.ZoneId;
059import java.time.ZonedDateTime;
060import java.util.*;
061
062import static io.stallion.utils.Literals.*;
063import static io.stallion.Context.*;
064
065/**
066 * A singleton that stores the database connection pool, registered model schemas,
067 * and provides a bunch of helper methods for actually talking to the database.
068 */
069public class DB {
070    //@Converter(converterName = "")
071    private List<Schema> tables = new ArrayList<Schema>();
072    private Map<String, Schema> classToSchema = new HashMap<String, Schema>();
073    private DataSource dataSource;
074    private Map<String, AttributeConverter> converters = new HashMap<String, AttributeConverter>();
075    private Tickets tickets;
076
077    private DbImplementation dbImplementation;
078    private static boolean useDummyPersisterForSqlGenerationMode = false;
079
080    private static DB _instance;
081
082    public static DB instance() {
083        return _instance;
084    }
085
086    public static boolean available() {
087        if (_instance != null) {
088            return true;
089        }
090        return false;
091    }
092
093
094
095    /**
096     * Load the database based on the configuration defined in the stallion.toml settings.
097     * @return
098     */
099    public static DB load() {
100        if (emptyInstance(Settings.instance().getDatabase()) || empty(Settings.instance().getDatabase().getUrl())) {
101            return null;
102        }
103        Properties p = new Properties(System.getProperties());
104        p.put("com.mchange.v2.log.MLog", "com.mchange.v2.log.FallbackMLog");
105        p.put("com.mchange.v2.log.FallbackMLog.DEFAULT_CUTOFF_LEVEL", "OFF"); // or any other
106        System.setProperties(p);
107
108        DbConfig config = Settings.instance().getDatabase();
109        DB db;
110        if (empty(config.getDbAccessorClass()) || "io.stallion.dataAccess.db.DB".equals(config.getDbAccessorClass())) {
111            db = new DB();
112        } else {
113            Class cls = StallionClassLoader.loadClass(config.getDbAccessorClass());
114            if (cls == null) {
115                throw new ConfigException("The dbAccessorClass you chose in the database settings could not be found: " + config.getDbAccessorClass());
116            }
117            try {
118                db = (DB)cls.newInstance();
119            } catch (InstantiationException e) {
120                throw new RuntimeException(e);
121            } catch (IllegalAccessException e) {
122                throw new RuntimeException(e);
123            }
124        }
125
126        db.initialize(config);
127
128
129        _instance = db;
130        return _instance;
131    }
132
133    /**
134     * Close the datasource pool, null out the singleton instance.
135     */
136    public static void shutdown() {
137        if (_instance != null) {
138            if (_instance.getDataSource() instanceof  ComboPooledDataSource) {
139                ((ComboPooledDataSource)_instance.getDataSource()).close();
140            }
141        }
142        _instance = null;
143    }
144
145    /**
146     * Intialize the database based on the passed in configuration object.
147     * @param config
148     */
149    public void initialize(DbConfig config) {
150        try {
151            dbImplementation = (DbImplementation)StallionClassLoader.loadClass(config.getImplementationClass()).newInstance();
152        } catch (InstantiationException e) {
153            throw new RuntimeException(e);
154        } catch (IllegalAccessException e) {
155            throw new RuntimeException(e);
156        }
157
158        // Test out the connection. We do this directly, because if we test via the ComboPooledDataSource
159        // exceptions will make the driver hang while retrying, and will also bury the underlying cause
160
161        try {
162            Driver driver = (Driver)StallionClassLoader.loadClass(config.getDriverClass()).newInstance();
163            Properties props = new Properties();
164            props.setProperty("user", config.getUsername());
165            props.setProperty("password", config.getPassword());
166            try (Connection conn = driver.connect(config.getUrl(), props)) {
167                Statement st = conn.createStatement();
168                ResultSet results = st.executeQuery("SELECT 1 AS oneCol");
169                results.next();
170                Long i = results.getLong("oneCol");
171                assert i == 1L;
172            }
173        } catch (SQLException e) {
174            throw new RuntimeException(e);
175        } catch (InstantiationException e) {
176            throw new RuntimeException(e);
177        } catch (IllegalAccessException e) {
178            throw new RuntimeException(e);
179        }
180
181
182
183        ComboPooledDataSource cpds = new ComboPooledDataSource();
184        /*
185        try {
186            try (Connection conn = cpds.getConnection()) {
187                Statement st = conn.createStatement();
188                ResultSet results = st.executeQuery("SELECT 1");
189                Long i = results.getLong(0);
190                assert i == 1L;
191            }
192        } catch (SQLException e) {
193            throw new RuntimeException(e);
194        }
195
196        */
197        try {
198            cpds.setDriverClass(config.getDriverClass()); //loads the jdbc driver
199        } catch (PropertyVetoException e) {
200            throw new RuntimeException(e);
201        }
202
203        String url = config.getUrl();
204        if (!url.contains("?")) {
205            url += "?";
206        }
207        // Assume the database server is in UTC
208        if (!url.contains("&useLegacyDatetimeCode=")) {
209            url += "&useLegacyDatetimeCode=false";
210        }
211        if (!url.contains("&serverTimezone=")) {
212            url += "&serverTimezone=UTC";
213        }
214        //&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
215
216        cpds.setJdbcUrl(url);
217        cpds.setUser(config.getUsername());
218        cpds.setPassword(config.getPassword());
219
220
221        cpds.setAcquireRetryAttempts(10);
222        cpds.setAcquireRetryDelay(200);
223        //cpds.setCheckoutTimeout(1);
224        // the settings below are optional -- c3p0 can work with defaults
225        cpds.setMinPoolSize(5);
226        cpds.setAcquireIncrement(5);
227        cpds.setMaxPoolSize(20);
228        cpds.setIdleConnectionTestPeriod(5000);
229        cpds.setTestConnectionOnCheckin(true);
230
231        this.dataSource = cpds;
232
233
234        // Make sure the database server time is UTC and in sync with the local server time
235        // or else stop execution to prevent nasty and insiduious errors.
236        //Timestamp date = this.queryScalar(dbImplementation.getCurrentTimeStampQuery());
237        Timestamp date = this.queryScalar(dbImplementation.getCurrentTimeStampQuery());
238        ZonedDateTime now = utcNow();
239        ZonedDateTime dbTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(date.getTime()), ZoneId.of("UTC"));
240
241        //LocalDateTime now = utcNow().toLocalDateTime();
242        ZonedDateTime max = now.plusMinutes(2);
243        ZonedDateTime min = now.minusMinutes(2);
244
245        //LocalDateTime dbTime = date.toLocalDateTime();
246        if (dbTime.isAfter(max) || dbTime.isBefore(min)) {
247            throw new ConfigException("The database CURRENT_TIMESTAMP() is mismatched with the server time. Db time is " + dbTime + ". Server time is "  + now + ". Make sure the database server is in UTC and that all your servers clocks are matched. ");
248        }
249
250
251        // Todo: why not lazy load converters???
252        registerConverter(new JsonMapConverter());
253        registerConverter(new JsonSetConverter());
254        registerConverter(new JsonObjectConverter());
255        registerConverter(new JsonListConverter());
256
257        this.tickets = dbImplementation.initTicketsService(this);
258
259    }
260
261    /**
262     * Instantiate an instance of a Tickets subclass, based on the database type
263     * @return
264     */
265    public Tickets newTicketsGeneratorInstance() {
266        return dbImplementation.initTicketsService(this);
267    }
268
269    public Class getDefaultPersisterClass() {
270        return DbPersister.class;
271    }
272
273    public DB registerConverter(AttributeConverter converter) {
274        converters.put(converter.getClass().getName(), converter);
275        return this;
276    }
277
278    /**
279     * Returns a QueryRunner instance associated with the underlying dataSource
280     * @return
281     */
282    public QueryRunner newQuery() {
283        return new QueryRunner(dataSource);
284    }
285
286    /**
287     * Update the database with this object, or insert it if it does not exist.
288     *
289     * @param obj
290     * @return
291     */
292    public Model save(Model obj) {
293
294        if (obj.getId() == null) {
295
296            insert(obj);
297            return obj;
298        } else if (obj.getIsNewInsert()) {
299            insert(obj);
300            obj.setIsNewInsert(false);
301            return obj;
302        } else {
303            int affected = update(obj);
304            if (affected == 0) {
305                insert(obj);
306            }
307            return obj;
308        }
309    }
310
311    /**
312     * Query the database with arbitrary SQL and return a scalar object (a string, number, boolean, etc).
313     *
314     * @param sql
315     * @param params
316     * @param <T>
317     * @return
318     */
319    public <T> T queryScalar(String sql, Object...params) {
320
321        QueryRunner runner = new QueryRunner(dataSource);
322        try {
323            return runner.query(sql, new ScalarHandler<T>(), params);
324        } catch (SQLException e) {
325            throw new RuntimeException(e);
326        }
327
328    }
329
330    /**
331     * Fetch all objects of the given model.
332     *
333     * @param model
334     * @param <T>
335     * @return
336     */
337    public <T extends Model> List<T> fetchAll(Class<T> model) {
338        return fetchAll(model, null, null);
339    }
340
341    /**
342     * Fetch the object with the given id.
343     *
344     * @param model
345     * @param id
346     * @param <T>
347     * @return
348     */
349    public <T> T fetchOne(Class<? extends T> model, Object id) {
350        return fetchOne(model, "id", id);
351    }
352
353    /**
354     * Fetch an object where field==value
355     *
356     * @param model
357     * @param field
358     * @param value
359     * @param <T>
360     * @return
361     */
362    public <T> T fetchOne(Class<? extends T> model, String field, Object value) {
363        Schema schema = getSchemaForModelClass(model);
364        QueryRunner runner = new QueryRunner(dataSource);
365        //ResultSetHandler handler = new BeanHandler(model);
366        ResultSetHandler handler = new ModelResultHandler(schema);
367        String sql = "SELECT * FROM " + schema.getName() + " WHERE " + field + "=? LIMIT 1";
368        T record = null;
369        try {
370            record = (T)runner.query(sql, handler, value);
371        } catch (SQLException e) {
372            throw new RuntimeException(e);
373        }
374        return record;
375    }
376
377    /**
378     * Fetch all objects where column field is of value
379     *
380     * @param model
381     * @param field
382     * @param value
383     * @param <T>
384     * @return
385     */
386    public <T extends Model> List<T> fetchAll(Class<? extends T> model, String field, Object value) {
387        Schema schema = getSchemaForModelClass(model);
388        QueryRunner runner = new QueryRunner(dataSource);
389        ModelListHandler<T> handler = new ModelListHandler<T>(schema);
390
391        String sql = "SELECT * FROM " + schema.getName();
392        if (!empty(field)) {
393            sql += " WHERE " + field + "=?";
394        }
395        List records = null;
396        try {
397            if (!empty(field)) {
398                records = runner.query(sql, handler, value);
399            } else {
400                records = runner.query(sql, handler);
401            }
402        } catch (SQLException e) {
403            throw new RuntimeException(e);
404        }
405        return records;
406    }
407
408    /**
409     * Fetch all objects, sorted
410     *
411     * @param model
412     * @param <T>
413     * @return
414     */
415    public <T extends Model> List<T> fetchAllSorted(Class<? extends T> model, String sortField, String sortDirection) {
416        Schema schema = getSchemaForModelClass(model);
417        QueryRunner runner = new QueryRunner(dataSource);
418        ModelListHandler<T> handler = new ModelListHandler<T>(schema);
419        sortDirection = sortDirection.toUpperCase();
420        if (!"ASC".equals(sortDirection) && !"DESC".equals(sortDirection)) {
421            throw new UsageException("Invalid sort direction: " + sortDirection);
422        }
423
424        if (!"id".equals(sortField)  && !"row_updated_at".equals(sortField) && !schema.getKeyNames().contains(sortField)) {
425            throw new UsageException("Sort field must be a database key. Sort field was: " + sortField + " on model " + model.getCanonicalName());
426        }
427
428        String sql = "SELECT * FROM " + schema.getName() + " ORDER BY " + sortField + " " + sortDirection;
429        List records = null;
430        try {
431            records = runner.query(sql, handler);
432        } catch (SQLException e) {
433            throw new RuntimeException(e);
434        }
435        return records;
436    }
437
438    /**
439     * Find a list of rows via arbitrary SQL, and return those rows as hashmaps
440     *
441     * @param sql
442     * @param args
443     * @return
444     */
445    public List<Map<String, Object>> findRecords(String sql, Object... args) {
446        QueryRunner runner = new QueryRunner(dataSource);
447        MapListHandler handler = new MapListHandler();
448        try {
449            return runner.query(sql, handler, args);
450        } catch (SQLException e) {
451            throw new RuntimeException(e);
452        }
453    }
454
455    /**
456     * Find one record via arbitrary SQL and return it as a hashmap
457     *
458     * @param sql
459     * @param args
460     * @return
461     */
462    public Map<String, Object> findRecord(String sql, Object... args) {
463        QueryRunner runner = new QueryRunner(dataSource);
464        MapHandler handler = new MapHandler();
465        try {
466            return runner.query(sql, handler, args);
467        } catch (SQLException e) {
468            throw new RuntimeException(e);
469        }
470    }
471
472    /**
473     * Find one object of the given model via arbitrary SQL
474     *
475     * @param model
476     * @param sql
477     * @param args
478     * @param <T>
479     * @return
480     */
481    public <T extends Model> T queryForOne(Class<T> model, String sql, Object ...args) {
482        List<T> models = query(model, sql, args);
483        if (models.size() < 1) {
484            return null;
485        } else {
486            return models.get(0);
487        }
488    }
489
490    /**
491     * Find a list of objects of the given model via arbitrary SQL
492     *
493     * @param model
494     * @param sql
495     * @param args
496     * @param <T>
497     * @return
498     */
499    public <T extends Model> List<T> query(Class<T> model, String sql, Object ...args) {
500        QueryRunner runner = new QueryRunner(dataSource);
501        Schema schema = null;
502        if (Model.class.isAssignableFrom(model)) {
503            schema = getSchemaForModelClass(model);
504        }
505        if (schema != null) {
506            ModelListHandler<T> handler = new ModelListHandler<T>(schema);
507            try {
508                return runner.query(sql, handler, args);
509            } catch (SQLException e) {
510                throw new RuntimeException(e);
511            }
512        } else {
513            BeanListHandler<T> handler = new BeanListHandler(model);
514            try {
515                return runner.query(sql, handler, args);
516            } catch (SQLException e) {
517                throw new RuntimeException(e);
518            }
519        }
520    }
521
522    /**
523     * Find a list of objects of the given model via arbitrary SQL.
524     * Accepts any java bean, does not require a Stallion Model
525     *
526     * @param model
527     * @param sql
528     * @param args
529     * @param <T>
530     * @return
531     */
532    public <T> List<T> queryBean(Class<T> model, String sql, Object ...args) {
533        QueryRunner runner = new QueryRunner(dataSource);
534        BeanListHandler<T> handler = new BeanListHandler(model);
535        try {
536            return runner.query(sql, handler, args);
537        } catch (SQLException e) {
538            throw new RuntimeException(e);
539        }
540    }
541
542    /**
543     * Find a list of objects via arbitrary SQL, checking the cache first, and storing to the
544     * cache if retrieved fromt he database.
545     *
546     * @param model
547     * @param sql
548     * @param args
549     * @param <T>
550     * @return
551     */
552    public <T extends Model> List<T> cachedQuery(Class<T> model, String sql, Object ...args) {
553        String cacheKey = buildCacheKey(model, sql, args);
554        Object result = SmartQueryCache.getSmart(model.getCanonicalName(), cacheKey);
555        if (result != null) {
556            return (List<T>)result;
557        }
558        QueryRunner runner = new QueryRunner(dataSource);
559        List<T> items = list();
560        try {
561            Schema schema = getSchemaForModelClass(model);
562            if (!emptyInstance(schema)) {
563                ModelListHandler<T> handler = new ModelListHandler<T>(schema);
564                items = runner.query(sql, handler, args);
565            } else {
566                BeanListHandler<T> handler = new BeanListHandler(model);
567                items = runner.query(sql, handler, args);
568            }
569            SmartQueryCache.set(model.getCanonicalName(), cacheKey, items);
570            return items;
571        } catch (SQLException e) {
572            SmartQueryCache.set(model.getCanonicalName(), cacheKey, items);
573            throw new RuntimeException(e);
574        }
575    }
576
577    /**
578     * Find a single value based on the given model, from arbitrary SQL, and
579     * use the cache.
580     *
581     * @param model
582     * @param sql
583     * @param params
584     * @param <T>
585     * @return
586     */
587    public <T> T cachedScalar(Class model, String sql, Object ...params) {
588        String cacheKey = buildCacheKey(model, sql, params);
589        T result = (T) SmartQueryCache.getSmart(model.getCanonicalName(), cacheKey);
590        if (result != null) {
591            return result;
592        }
593        QueryRunner runner = new QueryRunner(dataSource);
594        try {
595            result = runner.query(sql, new ScalarHandler<T>(), params);
596            SmartQueryCache.set(model.getCanonicalName(), cacheKey, result);
597            return result;
598        } catch (SQLException e) {
599            throw new RuntimeException(e);
600        }
601
602    }
603
604    /**
605     * Select from the given model, using just an arbitrary WHERE ... clause, and use the cache.
606     *
607     * @param model
608     * @param where
609     * @param args
610     * @param <T>
611     * @return
612     */
613    public <T extends Model> List<T> cachedWhere(Class<T> model, String where, Object ...args) {
614        String cacheKey = buildCacheKey(model, where, args);
615        Object result = SmartQueryCache.getSmart(model.getCanonicalName(), cacheKey);
616        if (result != null) {
617            return (List<T>)result;
618        }
619        Schema schema = getSchemaForModelClass(model);
620
621        QueryRunner runner = new QueryRunner(dataSource);
622        //ResultSetHandler handler = new BeanHandler(model);
623        String sql = "SELECT * FROM " + schema.getName() + " WHERE " + where;
624        ModelListHandler<T> handler = new ModelListHandler<T>(schema);
625        List<T> items = list();
626        try {
627            items = runner.query(sql, handler, args);
628
629            SmartQueryCache.set(model.getCanonicalName(), cacheKey, items);
630            return items;
631        } catch (SQLException e) {
632            SmartQueryCache.set(model.getCanonicalName(), cacheKey, items);
633            throw new RuntimeException(e);
634        }
635    }
636
637    /**
638     * Turns a SQL query into a unique md5 hash to be used as a cache key.
639     *
640     * @param model
641     * @param sql
642     * @param args
643     * @return
644     */
645    protected String buildCacheKey(Class model, String sql, Object ...args) {
646        StringBuilder builder = new StringBuilder();
647        builder.append("SQLQuery" + Literals.GSEP);
648        builder.append(model.getCanonicalName() + Literals.GSEP);
649        builder.append(sql + Literals.GSEP);
650        for (Object arg: args) {
651            builder.append(arg.toString() + Literals.GSEP);
652        }
653        String fullKey = builder.toString();
654        return DigestUtils.md5Hex(fullKey);
655    }
656
657
658    /**
659     * Find an object using an arbitrary SQL WHERE ... clause
660     *
661     * @param model
662     * @param where
663     * @param args
664     * @return
665     */
666    public Object where(Class model, String where, Object ...args) {
667        Schema schema = getSchemaForModelClass(model);
668
669        QueryRunner runner = new QueryRunner(dataSource);
670        //ResultSetHandler handler = new BeanHandler(model);
671        String sql = "SELECT * FROM \"" + schema.getName() + "\" WHERE " + where;
672        ModelListHandler handler = new ModelListHandler(schema);
673        try {
674            return runner.query(sql, handler, args);
675        } catch (SQLException e) {
676            throw new RuntimeException(e);
677        }
678    }
679
680    /**
681     * Delete the object from the database.
682     *
683     * @param obj
684     */
685    public void delete(Model obj) {
686        Schema schema = getSchemaForModelClass(obj.getClass());
687
688        String sql = "DELETE FROM " + schema.getName() + " WHERE id=?";
689        QueryRunner runner = new QueryRunner(dataSource);
690        try {
691            runner.update(sql, obj.getId());
692        } catch (SQLException e) {
693            throw new RuntimeException(e);
694        }
695        obj.setDeleted(true);
696    }
697
698    /**
699     * Update the object
700     *
701     * @param obj
702     * @return rows affected
703     */
704    public int update(Model obj) {
705        Schema schema = getSchemaForModelClass(obj.getClass());
706
707        String sql = "UPDATE `" + schema.getName() + "` SET ";
708        List args = new ArrayList<>();
709        for (Col col: schema.getColumns()) {
710            if (col.getUpdateable()) {
711                sql += "`" + col.getName() + "`" + "=?, ";
712                Object arg = PropertyUtils.getPropertyOrMappedValue(obj, col.getPropertyName());
713                arg = convertColumnArg(obj, col, arg);
714                args.add(arg);
715            }
716        }
717        sql = StringUtils.strip(sql.trim(), ",");
718        sql += " WHERE id=?";
719        args.add(obj.getId());
720        QueryRunner run = new QueryRunner( dataSource );
721        int affected = 0;
722        try {
723            affected = run.update(sql, args.toArray());
724        } catch (SQLException e) {
725            throw new RuntimeException(e);
726        }
727        return affected;
728    }
729
730    /**
731     * Update only the passed in key values
732     *
733     * @param obj
734     * @return rows affected
735     */
736    public int update(Model obj, Map<String, Object> values) {
737        if (values.size() == 0) {
738            return 0;
739        }
740        Schema schema = getSchemaForModelClass(obj.getClass());
741        String sql = "UPDATE `" + schema.getName() + "` SET ";
742        List args = new ArrayList<>();
743        for (Col col: schema.getColumns()) {
744            if (col.getUpdateable() && values.containsKey(col.getPropertyName())) {
745                sql += "`" + col.getName() + "`" + "=?, ";
746                Object arg = values.get(col.getPropertyName());
747                arg = convertColumnArg(obj, col, arg);
748                args.add(arg);
749            }
750        }
751        if (args.size() == 0) {
752            return 0;
753        }
754        sql = StringUtils.strip(sql.trim(), ",");
755        sql += " WHERE id=?";
756        args.add(obj.getId());
757        QueryRunner run = new QueryRunner( dataSource );
758        int affected = 0;
759        try {
760            affected = run.update(sql, args.toArray());
761        } catch (SQLException e) {
762            throw new RuntimeException(e);
763        }
764        return affected;
765    }
766
767    /**
768     * Insert the object into the database.
769     *
770     * @param obj
771     * @return
772     */
773    public Model insert(Model obj) {
774        Schema schema = getSchemaForModelClass(obj.getClass());
775
776        if (obj.getId() == null) {
777            obj.setId(dal().getTickets().nextId());
778        }
779        String sql = "INSERT INTO `" + schema.getName() + "`  (id ";
780        List args = new ArrayList<>();
781        args.add(obj.getId());
782        for(Col col: schema.getColumns()) {
783            if (col.getInsertable()) {
784                sql += ", `" + col.getName() + "` ";
785            }
786        }
787        sql += ") VALUES(?";
788        for(Col col: schema.getColumns()) {
789            if (col.getInsertable()) {
790                sql += ", ?";
791                Object arg = PropertyUtils.getPropertyOrMappedValue(obj, col.getPropertyName());
792                if (arg == null && col.getDefaultValue() != null) {
793                    arg = col.getDefaultValue();
794                    PropertyUtils.setProperty(obj, col.getPropertyName(), arg);
795                }
796                arg = convertColumnArg(obj, col, arg);
797                args.add(arg);
798            }
799        }
800        sql += ") ";
801        QueryRunner runner = new QueryRunner(dataSource);
802        try {
803            runner.update(sql, args.toArray());
804        } catch (SQLException e) {
805            throw new RuntimeException(e);
806        }
807        return obj;
808    }
809
810    /**
811     * Trys to convert java type into what is needed by JDBC to store to the database.
812     *
813     * @param o
814     * @param col
815     * @param arg
816     * @return
817     */
818    public Object convertColumnArg(Model o, Col col, Object arg) {
819
820        if (arg == null && col.getDefaultValue() != null) {
821            arg = col.getDefaultValue();
822        }
823        if (col.getConverter() != null) {
824            arg = col.getConverter().toDb(o, arg, col.getPropertyName());
825        } else if (arg != null && !StringUtils.isBlank(col.getConverterClassName())) {
826            AttributeConverter converter = this.getConverter(col.getConverterClassName());
827            arg = converter.convertToDatabaseColumn(arg);
828        } else if (arg instanceof LocalDateTime) {
829            arg = new Timestamp(((LocalDateTime) arg).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
830        } else if (arg instanceof ZonedDateTime) {
831            arg = new Timestamp(((ZonedDateTime) arg).toInstant().toEpochMilli());
832        } else if (arg instanceof Enum) {
833            return arg.toString();
834        }
835        return arg;
836    }
837
838    /**
839     * Add a model to the internal registry of schemas.
840     *
841     * @param cls
842     * @return
843     */
844    public Schema addModel(Class cls) {
845        Schema schema = modelToSchema(cls);
846        this.addTable(schema);
847        classToSchema.put(schema.getClazz().getName(), schema);
848        return schema;
849    }
850
851    /**
852     * Add a dynamicModelDefinition to the internal registry of schemas
853     *
854     * @param def
855     * @return
856     */
857    public Schema addDynamicModelDefinition(DynamicModelDefinition def) {
858        Schema schema = new Schema(def.getTable(), def.getModelClass());
859        schema.setColumns(def.getColumns());
860        this.addTable(schema);
861        classToSchema.put(def.getTable(), schema);
862        return schema;
863    }
864
865
866
867    Schema getSchemaForModelClass(Class cls) {
868        if (BaseJavascriptModel.class.isAssignableFrom(cls)) {
869            String bucketName = null;
870            try {
871                bucketName = ((BaseJavascriptModel) cls.newInstance()).getBucketName();
872            } catch (InstantiationException e) {
873                throw new RuntimeException(e);
874            } catch (IllegalAccessException e) {
875                throw new RuntimeException(e);
876            }
877            return classToSchema.getOrDefault(bucketName, null);
878        } else {
879            return classToSchema.getOrDefault(cls.getCanonicalName(), null);
880        }
881    }
882
883    public Schema modelToSchema(Class cls) {
884        try {
885            Field metaField = cls.getField("meta");
886            if (metaField != null) {
887                Map<String, Object> o = (Map<String, Object>)metaField.get(null);
888                if (o != null && !empty(o.getOrDefault("tableName", "").toString())) {
889                    return metaDataModelToSchema(cls, o);
890                }
891            }
892        } catch(NoSuchFieldException e) {
893            Log.exception(e, "Error finding meta field");
894        } catch(IllegalAccessException e) {
895            Log.exception(e, "Error accesing meta field");
896        }
897        return annotatedModelToSchema(cls);
898    }
899
900    private Schema annotatedModelToSchema(Class cls) {
901        Table table = (Table)cls.getDeclaredAnnotation(Table.class);
902        if (table == null) {
903            throw new UsageException("Trying to register model with database, but it has no @Table annotation: " + cls.getCanonicalName());
904        }
905        if (empty(table.name() )) {
906            throw new UsageException("Trying to register model with database, but it has no name for the @Table annotation: " + cls.getCanonicalName());
907        }
908        Schema schema = new Schema(table.name(), cls);
909        Object inst = null;
910        try {
911            inst = cls.newInstance();
912        } catch (InstantiationException e) {
913            throw new RuntimeException(e);
914        } catch (IllegalAccessException e) {
915            throw new RuntimeException(e);
916        }
917        ExtraKeyDefinitions keyDefinitions = (ExtraKeyDefinitions)cls.getDeclaredAnnotation(ExtraKeyDefinitions.class);
918        if (keyDefinitions != null) {
919            for(String def: keyDefinitions.value()) {
920                schema.getExtraKeyDefinitions().add(def);
921            }
922        }
923
924        for(Method method: cls.getMethods()) {
925            if (!method.getName().startsWith("get") && !method.getName().startsWith("is")) {
926                continue;
927            }
928            // If a property starts with "is", the type must be boolean, or else we continue;
929            if (method.getName().startsWith("is") &&
930                    (!method.getReturnType().isAssignableFrom(boolean.class)) && !method.getReturnType().isAssignableFrom(Boolean.class)) {
931                continue;
932            }
933
934            String propertyName = "";
935            if (method.getName().startsWith("is")) {
936                propertyName = method.getName().substring(2);
937            } else {
938                propertyName = method.getName().substring(3);
939            }
940            if (empty(propertyName)) {
941                continue;
942            }
943            propertyName = propertyName.substring(0, 1).toLowerCase() + propertyName.substring(1);
944            Column columnAnno = method.getAnnotation(Column.class);
945            if (columnAnno == null) {
946                continue;
947            }
948
949            String columnName = propertyName.toLowerCase();
950            if (!StringUtils.isBlank(columnAnno.name())) {
951                columnName = columnAnno.name();
952            }
953            Col col = new Col();
954            col.setPropertyName(propertyName);
955            col.setName(columnName);
956            col.setUpdateable(columnAnno.updatable());
957            col.setjType(method.getReturnType());
958            col.setInsertable(columnAnno.insertable());
959
960            col.setDbType(columnAnno.columnDefinition());
961            col.setNullable(columnAnno.nullable());
962            col.setLength(columnAnno.length());
963            // If the column cannot be null, we need a default value
964            if (!columnAnno.nullable()) {
965                col.setDefaultValue(PropertyUtils.getProperty(inst, propertyName));
966            }
967
968            Converter converterAnno = method.getDeclaredAnnotation(Converter.class);
969            Log.finest("Adding schema Column {0}", columnName);
970            if (converterAnno != null) {
971                Log.finest("ConverterAnno {0} {1} {2} ", columnName, converterAnno, converterAnno.name());
972                if (empty(converterAnno.name())) {
973                    col.setConverterClassName(converterAnno.cls().getCanonicalName());
974                } else {
975                    col.setConverterClassName(converterAnno.name());
976                }
977            }
978            if (method.getAnnotation(AlternativeKey.class) != null) {
979                col.setAlternativeKey(true);
980                schema.getKeyNames().add(col.getName());
981            }
982            if (method.getAnnotation(UniqueKey.class) != null) {
983                col.setUniqueKey(true);
984                schema.getKeyNames().add(col.getName());
985            }
986            if (columnAnno.unique()) {
987                col.setUniqueKey(true);
988                schema.getKeyNames().add(col.getName());
989            }
990
991            schema.getColumns().add(col);
992
993        }
994        return schema;
995    }
996
997    public List<Schema> getSchemas() {
998        return new ArrayList<>(classToSchema.values());
999    }
1000
1001    private Schema metaDataModelToSchema(Class cls, Map<String, Object> meta) {
1002        Schema schema = new Schema(meta.get("tableName").toString(), cls);
1003        for(Map colData: (List<Map>)meta.getOrDefault("columns", list())) {
1004            Col col = new Col();
1005            col.setPropertyName(colData.getOrDefault("propertyName", "").toString());
1006            col.setName(colData.getOrDefault("name", "").toString());
1007            col.setUpdateable((boolean) colData.getOrDefault("updateable", true));
1008            col.setInsertable((boolean) colData.getOrDefault("insertable", true));
1009
1010            schema.getColumns().add(col);
1011        }
1012
1013        return schema;
1014    }
1015
1016    /**
1017     * Execute an arbitrary SQL command, return the affected rows.
1018     *
1019     * @param sql
1020     * @param params
1021     * @return
1022     */
1023    public int execute(String sql, Object...params) {
1024        try {
1025            return newQuery().update(sql, params);
1026        } catch (SQLException e) {
1027            throw new RuntimeException(e);
1028        }
1029    }
1030
1031    DB addTable(Schema schema) {
1032        tables.add(schema);
1033        return this;
1034    }
1035
1036    /**
1037     * Get or instantiate an AttributeConverter instance based on the
1038     * converter className
1039     *
1040     * @param className
1041     * @return
1042     */
1043    public AttributeConverter getConverter(String className) {
1044        if (converters.containsKey(className)) {
1045            return converters.get(className);
1046        }
1047        Class cls = StallionClassLoader.loadClass(className);
1048        if (cls == null) {
1049            throw new UsageException("Could not find converter class: " + className);
1050        }
1051        try {
1052            AttributeConverter converter = (AttributeConverter)cls.newInstance();
1053            converters.put(className, converter);
1054            return converter;
1055        } catch (InstantiationException e) {
1056            throw new RuntimeException(e);
1057        } catch (IllegalAccessException e) {
1058            throw new RuntimeException(e);
1059        }
1060
1061    }
1062
1063    public boolean tableExists(String tableName) {
1064        if (tableName.contains("`") || tableName.contains("'")) {
1065            return false;
1066        }
1067        List<Map<String, Object>> tables = DB.instance().findRecords("SHOW TABLES LIKE '" + tableName + "'");
1068        return tables.size() > 0;
1069    }
1070
1071    public boolean columnExists(String tableName, String column) {
1072        if (!tableExists(tableName)) {
1073            return false;
1074        }
1075        if (tableName.contains("`")) {
1076            return false;
1077        }
1078        List<Map<String, Object>> columns = DB.instance().findRecords("SHOW COLUMNS FROM `" + tableName + "` WHERE field=?", column);
1079        return columns.size() > 0;
1080    }
1081
1082    /**
1083     * Get the table schema for a given model class
1084     *
1085     * @param cls
1086     * @return
1087     */
1088    public Schema getSchema(Class cls) {
1089        return classToSchema.get(cls.getName());
1090    }
1091
1092    /**
1093     * Get the table schema for a model name
1094     *
1095     * @param name
1096     * @return
1097     */
1098    public Schema getSchema(String name) {
1099        return classToSchema.get(name);
1100    }
1101
1102
1103    /**
1104     * Get the underlying database connection pool
1105     *
1106     * @return
1107     */
1108    public DataSource getDataSource() {
1109        return dataSource;
1110    }
1111
1112    public void setDataSource(DataSource dataSource) {
1113        this.dataSource = dataSource;
1114    }
1115
1116
1117    /**
1118     * Get the Tickets service for generating new IDS
1119     * @return
1120     */
1121    public Tickets getTickets() {
1122        return tickets;
1123    }
1124
1125
1126    public DbImplementation getDbImplementation() {
1127        return dbImplementation;
1128    }
1129
1130
1131    /**
1132     * If true, internally, DalRegistry.register() will never use a DbPersister, instead
1133     * it will use a DummyPersister. This flag is set to true if we are running the command
1134     * sql-generate, in which case we need to register all models, but the models will fail
1135     * because the table does not actually exist. So by using a DummyPersister, we avoid
1136     * hard failures, allowing model registry to execute, then we can get a list of all registered
1137     * models and generate the correct SQL.
1138     *
1139     * @return
1140     */
1141    public static boolean isUseDummyPersisterForSqlGenerationMode() {
1142        return useDummyPersisterForSqlGenerationMode;
1143    }
1144
1145    public static void setUseDummyPersisterForSqlGenerationMode(boolean useDummyPersisterForSqlGenerationMode) {
1146        DB.useDummyPersisterForSqlGenerationMode = useDummyPersisterForSqlGenerationMode;
1147    }
1148}