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}