Coverage Summary for Class: MetaData (net.sf.persism)
| Class | Class, % | Method, % | Line, % |
|---|---|---|---|
| MetaData | 100% (1/1) | 100% (30/30) | 94.1% (429/456) |
1 package net.sf.persism; 2 3 import net.sf.persism.annotations.*; 4 5 import java.lang.annotation.Annotation; 6 import java.lang.reflect.Field; 7 import java.lang.reflect.Method; 8 import java.lang.reflect.Modifier; 9 import java.sql.*; 10 import java.text.MessageFormat; 11 import java.util.*; 12 import java.util.concurrent.ConcurrentHashMap; 13 14 import static java.text.MessageFormat.format; 15 import static net.sf.persism.Util.*; 16 17 /** 18 * Meta data collected in a map singleton based on connection url 19 * 20 * @author Dan Howard 21 * @since 3/31/12 4:19 PM 22 */ 23 final class MetaData { 24 25 private static final Log log = Log.getLogger(MetaData.class); 26 27 // properties for each class - static because this won't change between MetaData instances 28 private static final Map<Class<?>, Collection<PropertyInfo>> propertyMap = new ConcurrentHashMap<>(32); 29 30 // column to property map for each class 31 private Map<Class<?>, Map<String, PropertyInfo>> propertyInfoMap = new ConcurrentHashMap<>(32); 32 private Map<Class<?>, Map<String, ColumnInfo>> columnInfoMap = new ConcurrentHashMap<>(32); 33 private Map<Class<?>, List<String>> propertyNames = new ConcurrentHashMap<>(32); 34 35 // table or view name for each class 36 private Map<Class<?>, String> tableOrViewMap = new ConcurrentHashMap<>(32); 37 38 // SQL for updates/inserts/deletes/selects for each class 39 private Map<Class<?>, String> updateStatementsMap = new ConcurrentHashMap<>(32); 40 private Map<Class<?>, String> insertStatementsMap = new ConcurrentHashMap<>(32); 41 private Map<Class<?>, String> deleteStatementsMap = new ConcurrentHashMap<>(32); 42 private Map<Class<?>, String> selectStatementsMap = new ConcurrentHashMap<>(32); 43 44 45 // Key is SQL with named params, Value is SQL with ? 46 // private Map<String, String> sqlWitNamedParams = new ConcurrentHashMap<String, String>(32); 47 48 // Key is SQL with named params, Value list of named params 49 // private Map<String, List<String>> namedParams = new ConcurrentHashMap<String, List<String>>(32); 50 51 // private Map<Class, List<String>> primaryKeysMap = new ConcurrentHashMap<Class, List<String>>(32); // remove later maybe? 52 53 // list of tables in the DB 54 private Set<String> tableNames = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); 55 56 // list of views in the DB 57 private Set<String> viewNames = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); 58 59 // Map of table names + meta data 60 // private Map<String, TableInfo> tableInfoMap = new ConcurrentHashMap<String, TableInfo>(32); 61 62 private static final Map<String, MetaData> metaData = new ConcurrentHashMap<String, MetaData>(4); 63 64 private ConnectionTypes connectionType; 65 66 // the "extra" characters that can be used in unquoted identifier names (those beyond a-z, A-Z, 0-9 and _) 67 // Was using DatabaseMetaData getExtraNameCharacters() but some drivers don't provide these and still allow 68 // for non alpha-numeric characters in column names. We'll just use a static set. 69 private static final String EXTRA_NAME_CHARACTERS = "`~!@#$%^&*()-+=/|\\{}[]:;'\".,<>*"; 70 71 private static final String SELECT_FOR_COLUMNS = "SELECT * FROM {0}{1}{2} WHERE 1=0"; 72 73 private MetaData(Connection con, String sessionKey) throws SQLException { 74 75 log.debug("MetaData CREATING instance [%s] ", sessionKey); 76 77 connectionType = ConnectionTypes.get(sessionKey); 78 if (connectionType == ConnectionTypes.Other) { 79 log.warn(Messages.UnknownConnectionType.message(con.getMetaData().getDatabaseProductName())); 80 } 81 populateTableList(con); 82 } 83 84 static synchronized MetaData getInstance(Connection con, String sessionKey) throws SQLException { 85 86 if (sessionKey == null) { 87 sessionKey = con.getMetaData().getURL(); 88 } 89 90 if (metaData.get(sessionKey) == null) { 91 metaData.put(sessionKey, new MetaData(con, sessionKey)); 92 } 93 log.debug("MetaData getting instance %s", sessionKey); 94 return metaData.get(sessionKey); 95 } 96 97 // Should only be called IF the map does not contain the column meta information yet. 98 // Version for Tables 99 private synchronized <T> Map<String, PropertyInfo> determinePropertyInfo(Class<T> objectClass, String tableName, Connection connection) { 100 // double check map 101 if (propertyInfoMap.containsKey(objectClass)) { 102 return propertyInfoMap.get(objectClass); 103 } 104 105 String sd = connectionType.getKeywordStartDelimiter(); 106 String ed = connectionType.getKeywordEndDelimiter(); 107 108 ResultSet rs = null; 109 Statement st = null; 110 try { 111 st = connection.createStatement(); 112 // gives us real column names with case. 113 String sql = MessageFormat.format(SELECT_FOR_COLUMNS, sd, tableName, ed); 114 if (log.isDebugEnabled()) { 115 log.debug("determineColumns: %s", sql); 116 } 117 rs = st.executeQuery(sql); 118 return determinePropertyInfo(objectClass, rs); 119 } catch (SQLException e) { 120 throw new PersismException(e.getMessage(), e); 121 } finally { 122 cleanup(st, rs); 123 } 124 } 125 126 // Should only be called IF the map does not contain the column meta information yet. 127 private synchronized <T> Map<String, PropertyInfo> determinePropertyInfo(Class<T> objectClass, ResultSet rs) { 128 // double check map - note this could be called with a Query were we never have that in here 129 if (propertyInfoMap.containsKey(objectClass)) { 130 return propertyInfoMap.get(objectClass); 131 } 132 133 List<String> propertyNames = new ArrayList<>(32); 134 try { 135 ResultSetMetaData rsmd = rs.getMetaData(); 136 Collection<PropertyInfo> properties = getPropertyInfo(objectClass); 137 138 int columnCount = rsmd.getColumnCount(); 139 //Map<String, PropertyInfo> columns = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); 140 Map<String, PropertyInfo> columns = new LinkedHashMap<>(columnCount); 141 for (int j = 1; j <= columnCount; j++) { 142 String realColumnName = rsmd.getColumnLabel(j); 143 String columnName = realColumnName.toLowerCase().replace("_", "").replace(" ", ""); 144 // also replace these characters 145 for (int x = 0; x < EXTRA_NAME_CHARACTERS.length(); x++) { 146 columnName = columnName.replace("" + EXTRA_NAME_CHARACTERS.charAt(x), ""); 147 } 148 PropertyInfo foundProperty = null; 149 for (PropertyInfo propertyInfo : properties) { 150 String propertyName = propertyInfo.propertyName.toLowerCase().replace("_", ""); 151 if (propertyName.equalsIgnoreCase(columnName)) { 152 foundProperty = propertyInfo; 153 break; 154 } else { 155 // check annotation against column name 156 Annotation annotation = propertyInfo.getAnnotation(Column.class); 157 if (annotation != null) { 158 if (((Column) annotation).name().equalsIgnoreCase(realColumnName)) { 159 foundProperty = propertyInfo; 160 break; 161 } 162 } 163 } 164 } 165 166 if (foundProperty != null) { 167 columns.put(realColumnName, foundProperty); 168 propertyNames.add(foundProperty.propertyName); 169 } else { 170 log.warn(Messages.NoPropertyFoundForColumn.message(realColumnName, objectClass)); 171 } 172 } 173 174 // Do not put query classes into the metadata. It's possible the 1st run has a query with missing columns 175 // any calls afterward would fail because I never would refresh the columns again. Table is fine since we 176 // can do a SELECT * to get all columns up front but we can't do that with a query. 177 if (objectClass.getAnnotation(NotTable.class) == null) { 178 propertyInfoMap.put(objectClass, columns); 179 } 180 this.propertyNames.put(objectClass, propertyNames); 181 182 return columns; 183 184 } catch (SQLException e) { 185 throw new PersismException(e.getMessage(), e); 186 } 187 } 188 189 @SuppressWarnings({"JDBCExecuteWithNonConstantString", "SqlDialectInspection"}) 190 private synchronized <T> Map<String, ColumnInfo> determineColumnInfo(Class<T> objectClass, String tableName, Connection connection) { 191 if (columnInfoMap.containsKey(objectClass)) { 192 return columnInfoMap.get(objectClass); 193 } 194 195 Statement st = null; 196 ResultSet rs = null; 197 Map<String, PropertyInfo> properties = getTableColumnsPropertyInfo(objectClass, connection); 198 String sd = connectionType.getKeywordStartDelimiter(); 199 String ed = connectionType.getKeywordEndDelimiter(); 200 201 try { 202 203 st = connection.createStatement(); 204 rs = st.executeQuery(format("SELECT * FROM {0}{1}{2} WHERE 1=0", sd, tableName, ed)); 205 206 // Make sure primary keys sorted by column order in case we have more than 1 207 // then we'll know the order to apply the parameters. 208 Map<String, ColumnInfo> map = new LinkedHashMap<>(32); 209 210 boolean primaryKeysFound = false; 211 212 // Grab all columns and make first pass to detect primary auto-inc 213 ResultSetMetaData rsMetaData = rs.getMetaData(); 214 for (int i = 1; i <= rsMetaData.getColumnCount(); i++) { 215 // only include columns where we have a property 216 if (properties.containsKey(rsMetaData.getColumnLabel(i))) { 217 ColumnInfo columnInfo = new ColumnInfo(); 218 columnInfo.columnName = rsMetaData.getColumnLabel(i); 219 columnInfo.autoIncrement = rsMetaData.isAutoIncrement(i); 220 columnInfo.primary = columnInfo.autoIncrement; 221 columnInfo.sqlColumnType = rsMetaData.getColumnType(i); 222 columnInfo.sqlColumnTypeName = rsMetaData.getColumnTypeName(i); 223 columnInfo.columnType = Types.convert(columnInfo.sqlColumnType); 224 columnInfo.length = rsMetaData.getColumnDisplaySize(i); 225 226 if (!primaryKeysFound) { 227 primaryKeysFound = columnInfo.primary; 228 } 229 230 PropertyInfo propertyInfo = properties.get(rsMetaData.getColumnLabel(i)); 231 Annotation annotation = propertyInfo.getAnnotation(Column.class); 232 233 if (annotation != null) { 234 Column col = (Column) annotation; 235 if (col.hasDefault()) { 236 columnInfo.hasDefault = true; 237 } 238 239 if (col.primary()) { 240 columnInfo.primary = true; 241 } 242 243 if (col.autoIncrement()) { 244 columnInfo.autoIncrement = true; 245 if (!columnInfo.columnType.isEligibleForAutoinc()) { 246 // This will probably cause some error or other problem. Notify the user. 247 log.warn(Messages.ColumnAnnotatedAsAutoIncButNAN.message(columnInfo.columnName, columnInfo.columnType)); 248 } 249 } 250 251 if (!primaryKeysFound) { 252 primaryKeysFound = columnInfo.primary; 253 } 254 } 255 256 map.put(columnInfo.columnName, columnInfo); 257 } 258 } 259 rs.close(); 260 261 DatabaseMetaData dmd = connection.getMetaData(); 262 if (objectClass.getAnnotation(View.class) == null) { 263 // Iterate primary keys and update column infos 264 rs = dmd.getPrimaryKeys(null, connectionType.getSchemaPattern(), tableName); 265 int primaryKeysCount = 0; 266 while (rs.next()) { 267 ColumnInfo columnInfo = map.get(rs.getString("COLUMN_NAME")); 268 if (columnInfo != null) { 269 columnInfo.primary = true; 270 271 if (!primaryKeysFound) { 272 primaryKeysFound = columnInfo.primary; 273 } 274 } 275 primaryKeysCount++; 276 } 277 278 if (primaryKeysCount == 0) { 279 log.warn(Messages.DatabaseMetaDataCouldNotFindPrimaryKeys.message(tableName)); 280 } 281 } 282 283 /* 284 Get columns from database metadata since we don't get Type from resultSetMetaData 285 with SQLite. + We also need to know if there's a default on a column. 286 */ 287 rs = dmd.getColumns(null, connectionType.getSchemaPattern(), tableName, null); 288 int columnsCount = 0; 289 while (rs.next()) { 290 ColumnInfo columnInfo = map.get(rs.getString("COLUMN_NAME")); 291 if (columnInfo != null) { 292 if (!columnInfo.hasDefault) { 293 columnInfo.hasDefault = containsColumn(rs, "COLUMN_DEF") && rs.getString("COLUMN_DEF") != null; 294 } 295 296 // Do we not have autoinc info here? Yes. 297 // IS_AUTOINCREMENT = NO or YES 298 if (!columnInfo.autoIncrement) { 299 columnInfo.autoIncrement = containsColumn(rs, "IS_AUTOINCREMENT") && "YES".equalsIgnoreCase(rs.getString("IS_AUTOINCREMENT")); 300 } 301 302 // Re-assert the type since older version of SQLite could not detect types with empty resultsets 303 // It seems OK now in the newer JDBC driver. 304 // See testTypes unit test in TestSQLite 305 if (containsColumn(rs, "DATA_TYPE")) { 306 columnInfo.sqlColumnType = rs.getInt("DATA_TYPE"); 307 if (containsColumn(rs, "TYPE_NAME")) { 308 columnInfo.sqlColumnTypeName = rs.getString("TYPE_NAME"); 309 } 310 columnInfo.columnType = Types.convert(columnInfo.sqlColumnType); 311 } 312 } 313 columnsCount++; 314 } 315 rs.close(); 316 317 if (columnsCount == 0) { 318 log.warn(Messages.DatabaseMetaDataCouldNotFindColumns.message(tableName)); 319 } 320 321 // FOR Oracle which doesn't set autoinc in metadata even if we have: 322 // "ID" NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY 323 // Apparently that's not enough for the Oracle JDBC driver to indicate this is autoinc. 324 // If we have a primary that's NUMERIC and HAS a default AND autoinc is not set then set it. 325 if (connectionType == ConnectionTypes.Oracle) { 326 Optional<ColumnInfo> autoInc = map.values().stream().filter(e -> e.autoIncrement).findFirst(); 327 if (!autoInc.isPresent()) { 328 // Do a second check if we have a primary that's numeric with a default. 329 Optional<ColumnInfo> primaryOpt = map.values().stream().filter(e -> e.primary).findFirst(); 330 if (primaryOpt.isPresent()) { 331 ColumnInfo primary = primaryOpt.get(); 332 if (primary.columnType.isEligibleForAutoinc() && primary.hasDefault) { 333 primary.autoIncrement = true; 334 primaryKeysFound = true; 335 } 336 } 337 } 338 } 339 340 if (!primaryKeysFound && objectClass.getAnnotation(View.class) == null) { 341 // Should we fail-fast? Actually no, we should not fail here. 342 // It's very possible the user has a table that they will never 343 // update, delete or select (by primary). 344 // They may only want to do read operations with specified queries and in that 345 // context we don't need any primary keys. (same with insert) 346 log.warn(Messages.NoPrimaryKeyFoundForTable.message(tableName)); 347 } 348 349 columnInfoMap.put(objectClass, map); 350 351 return map; 352 353 } catch (SQLException e) { 354 throw new PersismException(e.getMessage(), e); 355 } finally { 356 cleanup(st, rs); 357 } 358 } 359 360 static <T> Collection<PropertyInfo> getPropertyInfo(Class<T> objectClass) { 361 if (propertyMap.containsKey(objectClass)) { 362 return propertyMap.get(objectClass); 363 } 364 return determinePropertyInfo(objectClass); 365 } 366 367 private static synchronized <T> Collection<PropertyInfo> determinePropertyInfo(Class<T> objectClass) { 368 if (propertyMap.containsKey(objectClass)) { 369 return propertyMap.get(objectClass); 370 } 371 372 Map<String, PropertyInfo> propertyInfos = new HashMap<>(32); 373 374 List<Field> fields = new ArrayList<>(32); 375 376 // getDeclaredFields does not get fields from super classes..... 377 fields.addAll(Arrays.asList(objectClass.getDeclaredFields())); 378 Class<?> sup = objectClass.getSuperclass(); 379 log.debug("fields for %s", sup); 380 while (!sup.equals(Object.class) && !sup.equals(PersistableObject.class)) { 381 fields.addAll(Arrays.asList(sup.getDeclaredFields())); 382 sup = sup.getSuperclass(); 383 log.debug("fields for %s", sup); 384 } 385 386 Method[] methods = objectClass.getMethods(); 387 388 for (Field field : fields) { 389 // Skip static fields 390 if (Modifier.isStatic(field.getModifiers())) { 391 continue; 392 } 393 // log.debug("Field Name: %s", field.getName()); 394 String propertyName = field.getName(); 395 // log.debug("Property Name: *%s* ", propertyName); 396 397 PropertyInfo propertyInfo = new PropertyInfo(); 398 propertyInfo.propertyName = propertyName; 399 propertyInfo.field = field; 400 Annotation[] annotations = field.getAnnotations(); 401 for (Annotation annotation : annotations) { 402 propertyInfo.annotations.put(annotation.annotationType(), annotation); 403 } 404 405 for (Method method : methods) { 406 String propertyNameToTest = field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1); 407 // log.debug("property name for testing %s", propertyNameToTest); 408 if (propertyNameToTest.startsWith("Is") && propertyNameToTest.length() > 2 && Character.isUpperCase(propertyNameToTest.charAt(2))) { 409 propertyNameToTest = propertyName.substring(2); 410 } 411 412 String[] candidates = {"set" + propertyNameToTest, "get" + propertyNameToTest, "is" + propertyNameToTest, field.getName()}; 413 414 if (Arrays.asList(candidates).contains(method.getName())) { 415 //log.debug(" METHOD: %s", method.getName()); 416 417 annotations = method.getAnnotations(); 418 for (Annotation annotation : annotations) { 419 propertyInfo.annotations.put(annotation.annotationType(), annotation); 420 } 421 422 if (method.getName().equalsIgnoreCase("set" + propertyNameToTest)) { 423 propertyInfo.setter = method; 424 } else { 425 propertyInfo.getter = method; 426 } 427 } 428 } 429 430 propertyInfo.readOnly = propertyInfo.setter == null; 431 propertyInfos.put(propertyName.toLowerCase(), propertyInfo); 432 } 433 434 // Remove any properties found with the NotColumn annotation 435 // http://stackoverflow.com/questions/2026104/hashmap-keyset-foreach-and-remove 436 Iterator<Map.Entry<String, PropertyInfo>> it = propertyInfos.entrySet().iterator(); 437 while (it.hasNext()) { 438 Map.Entry<String, PropertyInfo> entry = it.next(); 439 PropertyInfo info = entry.getValue(); 440 if (info.getAnnotation(NotColumn.class) != null) { 441 it.remove(); 442 } 443 } 444 445 Collection<PropertyInfo> properties = Collections.unmodifiableCollection(propertyInfos.values()); 446 propertyMap.put(objectClass, properties); 447 return properties; 448 } 449 450 private static final String[] tableTypes = {"TABLE"}; 451 private static final String[] viewTypes = {"VIEW"}; 452 453 // Populates the tables list with table names from the DB. 454 // This list is used for discovery of the table name from a class. 455 // ONLY to be called from Init in a synchronized way. 456 private void populateTableList(Connection con) throws PersismException { 457 458 ResultSet rs = null; 459 460 try { 461 // NULL POINTER WITH 462 // http://social.msdn.microsoft.com/Forums/en-US/sqldataaccess/thread/5c74094a-8506-4278-ac1c-f07d1bfdb266 463 // solution: 464 // http://stackoverflow.com/questions/8988945/java7-sqljdbc4-sql-error-08s01-on-getconnection 465 466 rs = con.getMetaData().getTables(null, connectionType.getSchemaPattern(), null, tableTypes); 467 while (rs.next()) { 468 tableNames.add(rs.getString("TABLE_NAME")); 469 } 470 471 rs = con.getMetaData().getTables(null, connectionType.getSchemaPattern(), null, viewTypes); 472 while (rs.next()) { 473 viewNames.add(rs.getString("TABLE_NAME")); 474 } 475 476 } catch (SQLException e) { 477 throw new PersismException(e.getMessage(), e); 478 479 } finally { 480 cleanup(null, rs); 481 } 482 } 483 484 /** 485 * @param object 486 * @param connection 487 * @return sql update string 488 * @throws NoChangesDetectedForUpdateException if the data object implements Persistable and there are no changes detected 489 */ 490 String getUpdateStatement(Object object, Connection connection) throws PersismException, NoChangesDetectedForUpdateException { 491 492 if (object instanceof Persistable) { 493 Map<String, PropertyInfo> changes = getChangedProperties((Persistable<?>) object, connection); 494 if (changes.size() == 0) { 495 throw new NoChangesDetectedForUpdateException(); 496 } 497 // Note we don't not add Persistable updates to updateStatementsMap since they will be different each time. 498 String sql = buildUpdateString(object, changes.keySet().iterator(), connection); 499 if (log.isDebugEnabled()) { 500 log.debug("getUpdateStatement for %s for changed fields is %s", object.getClass(), sql); 501 } 502 return sql; 503 } 504 505 String sql; 506 if (updateStatementsMap.containsKey(object.getClass())) { 507 sql = updateStatementsMap.get(object.getClass()); 508 } else { 509 sql = determineUpdateStatement(object, connection); 510 } 511 if (log.isDebugEnabled()) { 512 log.debug("getUpdateStatement for: %s %s", object.getClass(), sql); 513 } 514 return sql; 515 } 516 517 // Used by Objects not implementing Persistable since they will always use the same update statement 518 private synchronized String determineUpdateStatement(Object object, Connection connection) { 519 if (updateStatementsMap.containsKey(object.getClass())) { 520 return updateStatementsMap.get(object.getClass()); 521 } 522 523 Map<String, PropertyInfo> columns = getTableColumnsPropertyInfo(object.getClass(), connection); 524 525 String updateStatement = buildUpdateString(object, columns.keySet().iterator(), connection); 526 527 // Store static update statement for future use. 528 updateStatementsMap.put(object.getClass(), updateStatement); 529 530 if (log.isDebugEnabled()) { 531 log.debug("determineUpdateStatement for %s is %s", object.getClass(), updateStatement); 532 } 533 534 return updateStatement; 535 } 536 537 538 // Note this will not include columns unless they have the associated property. 539 String getInsertStatement(Object object, Connection connection) throws PersismException { 540 String sql; 541 542 if (insertStatementsMap.containsKey(object.getClass())) { 543 sql = insertStatementsMap.get(object.getClass()); 544 } else { 545 sql = determineInsertStatement(object, connection); 546 } 547 548 if (log.isDebugEnabled()) { 549 log.debug("getInsertStatement for: %s %s", object.getClass(), sql); 550 } 551 return sql; 552 } 553 554 private synchronized String determineInsertStatement(Object object, Connection connection) { 555 if (insertStatementsMap.containsKey(object.getClass())) { 556 return insertStatementsMap.get(object.getClass()); 557 } 558 559 try { 560 String tableName = getTableName(object.getClass(), connection); 561 String sd = connectionType.getKeywordStartDelimiter(); 562 String ed = connectionType.getKeywordEndDelimiter(); 563 564 Map<String, ColumnInfo> columns = getColumns(object.getClass(), connection); 565 Map<String, PropertyInfo> properties = getTableColumnsPropertyInfo(object.getClass(), connection); 566 567 StringBuilder sbi = new StringBuilder(); 568 sbi.append("INSERT INTO ").append(sd).append(tableName).append(ed).append(" ("); 569 570 StringBuilder sbp = new StringBuilder(); 571 sbp.append(") VALUES ("); 572 573 String sep = ""; 574 boolean saveInMap = true; 575 576 for (ColumnInfo column : columns.values()) { 577 if (!column.autoIncrement) { 578 579 if (column.hasDefault) { 580 581 saveInMap = false; 582 583 // Do not include if this column has a default and no value has been 584 // set on it's associated property. 585 if (properties.get(column.columnName).getter.invoke(object) == null) { 586 continue; 587 } 588 589 } 590 591 sbi.append(sep).append(sd).append(column.columnName).append(ed); 592 sbp.append(sep).append("?"); 593 sep = ", "; 594 } 595 } 596 597 sbi.append(sbp).append(") "); 598 599 String insertStatement; 600 insertStatement = sbi.toString(); 601 602 if (log.isDebugEnabled()) { 603 log.debug("determineInsertStatement for %s is %s", object.getClass(), insertStatement); 604 } 605 606 // Do not put this insert statement into the map if any columns have defaults 607 // because the insert statement will vary by different instances of the data object. 608 if (saveInMap) { 609 insertStatementsMap.put(object.getClass(), insertStatement); 610 } else { 611 insertStatementsMap.remove(object.getClass()); // remove just in case 612 } 613 614 return insertStatement; 615 616 } catch (Exception e) { 617 throw new PersismException(e.getMessage(), e); 618 } 619 } 620 621 String getDeleteStatement(Object object, Connection connection) { 622 if (deleteStatementsMap.containsKey(object.getClass())) { 623 return deleteStatementsMap.get(object.getClass()); 624 } 625 return determineDeleteStatement(object, connection); 626 } 627 628 private synchronized String determineDeleteStatement(Object object, Connection connection) { 629 if (deleteStatementsMap.containsKey(object.getClass())) { 630 return deleteStatementsMap.get(object.getClass()); 631 } 632 633 String tableName = getTableName(object.getClass(), connection); 634 String sd = connectionType.getKeywordStartDelimiter(); 635 String ed = connectionType.getKeywordEndDelimiter(); 636 637 List<String> primaryKeys = getPrimaryKeys(object.getClass(), connection); 638 639 StringBuilder sb = new StringBuilder(); 640 sb.append("DELETE FROM ").append(sd).append(tableName).append(ed).append(" WHERE "); 641 String sep = ""; 642 for (String column : primaryKeys) { 643 sb.append(sep).append(sd).append(column).append(ed).append(" = ?"); 644 sep = " AND "; 645 } 646 647 String deleteStatement = sb.toString(); 648 649 if (log.isDebugEnabled()) { 650 log.debug("determineDeleteStatement for %s is %s", object.getClass(), deleteStatement); 651 } 652 653 deleteStatementsMap.put(object.getClass(), deleteStatement); 654 655 return deleteStatement; 656 } 657 658 String getSelectStatement(Class<?> objectClass, Connection connection) { 659 if (selectStatementsMap.containsKey(objectClass)) { 660 return selectStatementsMap.get(objectClass); 661 } 662 return determineSelectStatement(objectClass, connection); 663 } 664 665 private synchronized String determineSelectStatement(Class<?> objectClass, Connection connection) { 666 667 if (selectStatementsMap.containsKey(objectClass)) { 668 return selectStatementsMap.get(objectClass); 669 } 670 671 String sd = connectionType.getKeywordStartDelimiter(); 672 String ed = connectionType.getKeywordEndDelimiter(); 673 674 String tableName = getTableName(objectClass, connection); 675 676 List<String> primaryKeys = getPrimaryKeys(objectClass, connection); 677 678 StringBuilder sb = new StringBuilder(); 679 sb.append("SELECT "); 680 681 String sep = ""; 682 683 Map<String, ColumnInfo> columns = getColumns(objectClass, connection); 684 for (String column : columns.keySet()) { 685 ColumnInfo columnInfo = columns.get(column); 686 sb.append(sep).append(sd).append(columnInfo.columnName).append(ed); 687 sep = ", "; 688 } 689 sb.append(" FROM ").append(sd).append(tableName).append(ed).append(" WHERE "); 690 691 sep = ""; 692 for (String column : primaryKeys) { 693 sb.append(sep).append(sd).append(column).append(ed).append(" = ?"); 694 sep = " AND "; 695 } 696 697 String selectStatement = sb.toString(); 698 699 if (log.isDebugEnabled()) { 700 log.debug("determineSelectStatement for %s is %s", objectClass, selectStatement); 701 } 702 703 selectStatementsMap.put(objectClass, selectStatement); 704 705 return selectStatement; 706 } 707 708 private String buildUpdateString(Object object, Iterator<String> it, Connection connection) throws PersismException { 709 String tableName = getTableName(object.getClass(), connection); 710 String sd = connectionType.getKeywordStartDelimiter(); 711 String ed = connectionType.getKeywordEndDelimiter(); 712 713 List<String> primaryKeys = getPrimaryKeys(object.getClass(), connection); 714 715 StringBuilder sb = new StringBuilder(); 716 sb.append("UPDATE ").append(sd).append(tableName).append(ed).append(" SET "); 717 String sep = ""; 718 719 Map<String, ColumnInfo> columns = getColumns(object.getClass(), connection); 720 while (it.hasNext()) { 721 String column = it.next(); 722 ColumnInfo columnInfo = columns.get(column); 723 if (columnInfo.autoIncrement || columnInfo.primary) { 724 log.info("buildUpdateString: skipping " + column); 725 } else { 726 sb.append(sep).append(sd).append(column).append(ed).append(" = ?"); 727 sep = ", "; 728 } 729 } 730 sb.append(" WHERE "); 731 sep = ""; 732 for (String column : primaryKeys) { 733 sb.append(sep).append(sd).append(column).append(ed).append(" = ?"); 734 sep = " AND "; 735 } 736 return sb.toString(); 737 } 738 739 Map<String, PropertyInfo> getChangedProperties(Persistable<?> persistable, Connection connection) throws PersismException { 740 741 try { 742 Persistable<?> original = (Persistable<?>) persistable.readOriginalValue(); 743 744 Map<String, PropertyInfo> columns = getTableColumnsPropertyInfo(persistable.getClass(), connection); 745 746 if (original == null) { 747 // Could happen in the case of cloning or other operation - so it's never read so it never sets original. 748 return columns; 749 } else { 750 Map<String, PropertyInfo> changedColumns = new HashMap<>(columns.keySet().size()); 751 for (String column : columns.keySet()) { 752 753 PropertyInfo propertyInfo = columns.get(column); 754 755 Object newValue = null; 756 Object orgValue = null; 757 newValue = propertyInfo.getter.invoke(persistable); 758 orgValue = propertyInfo.getter.invoke(original); 759 760 if (newValue != null && !newValue.equals(orgValue) || orgValue != null && !orgValue.equals(newValue)) { 761 changedColumns.put(column, propertyInfo); 762 } 763 } 764 return changedColumns; 765 } 766 767 } catch (Exception e) { 768 throw new PersismException(e.getMessage(), e); 769 } 770 } 771 772 <T> Map<String, ColumnInfo> getColumns(Class<T> objectClass, Connection connection) throws PersismException { 773 // Realistically at this point this objectClass will always be in the map since it's defined early 774 // when we get the table name but I'll double check it for determineColumnInfo anyway. 775 if (columnInfoMap.containsKey(objectClass)) { 776 return columnInfoMap.get(objectClass); 777 } 778 return determineColumnInfo(objectClass, getTableName(objectClass), connection); 779 } 780 781 <T> Map<String, PropertyInfo> getQueryColumnsPropertyInfo(Class<T> objectClass, ResultSet rs) throws PersismException { 782 // should not be mapped since ResultSet could contain different # of columns at different times. 783 // if (propertyInfoMap.containsKey(objectClass)) { 784 // return propertyInfoMap.get(objectClass); 785 // } 786 787 return determinePropertyInfo(objectClass, rs); 788 } 789 790 <T> Map<String, PropertyInfo> getTableColumnsPropertyInfo(Class<T> objectClass, Connection connection) throws PersismException { 791 if (propertyInfoMap.containsKey(objectClass)) { 792 return propertyInfoMap.get(objectClass); 793 } 794 return determinePropertyInfo(objectClass, getTableName(objectClass), connection); 795 } 796 797 <T> String getTableName(Class<T> objectClass) { 798 799 if (tableOrViewMap.containsKey(objectClass)) { 800 return tableOrViewMap.get(objectClass); 801 } 802 803 return determineTable(objectClass); 804 } 805 806 <T> List<String> getPropertyNames(Class<T> objectClass) { 807 return propertyNames.get(objectClass); 808 } 809 810 // internal version to retrieve meta information about this table's columns 811 // at the same time we find the table name itself. 812 private <T> String getTableName(Class<T> objectClass, Connection connection) { 813 814 String tableName = getTableName(objectClass); 815 816 if (!columnInfoMap.containsKey(objectClass)) { 817 determineColumnInfo(objectClass, tableName, connection); 818 } 819 820 if (!propertyInfoMap.containsKey(objectClass)) { 821 determinePropertyInfo(objectClass, tableName, connection); 822 } 823 return tableName; 824 } 825 826 private synchronized <T> String determineTable(Class<T> objectClass) { 827 828 if (tableOrViewMap.containsKey(objectClass)) { 829 return tableOrViewMap.get(objectClass); 830 } 831 832 String tableName; 833 final Table tableAnnotation = objectClass.getAnnotation(Table.class); 834 final View viewAnnotation = objectClass.getAnnotation(View.class); 835 836 if (tableAnnotation != null) { 837 Optional<String> foundTable = tableNames.stream().filter(s -> s.equalsIgnoreCase(tableAnnotation.value())).findFirst(); 838 if (foundTable.isPresent()) { 839 // get the actual case of the name 840 tableName = foundTable.get(); 841 } else { 842 throw new PersismException(Messages.CouldNotFindTableNameInTheDatabase.message(tableAnnotation.value(), objectClass.getName())); 843 } 844 } else if (viewAnnotation != null && isNotEmpty(viewAnnotation.value())) { 845 Optional<String> foundTable = viewNames.stream().filter(s -> s.equalsIgnoreCase(viewAnnotation.value())).findFirst(); 846 if (foundTable.isPresent()) { 847 // get the actual case of the name 848 tableName = foundTable.get(); 849 } else { 850 throw new PersismException(Messages.CouldNotFindViewNameInTheDatabase.message(viewAnnotation.value(), objectClass.getName())); 851 } 852 853 } else { 854 tableName = guessTableOrViewName(objectClass); 855 } 856 tableOrViewMap.put(objectClass, tableName); 857 return tableName; 858 } 859 860 // Returns the table name found in the DB in the same case as in the DB. 861 // throws PersismException if we cannot guess any table name for this class. 862 private <T> String guessTableOrViewName(Class<T> objectClass) throws PersismException { 863 Set<String> guesses = new LinkedHashSet<>(6); // guess order is important 864 List<String> guessedTables = new ArrayList<String>(6); 865 866 String className = objectClass.getSimpleName(); 867 868 Set<String> list; 869 boolean isView = false; 870 if (objectClass.getAnnotation(View.class) != null) { 871 list = viewNames; 872 isView = true; 873 } else { 874 list = tableNames; 875 } 876 877 addTableGuesses(className, guesses); 878 for (String tableName : list) { 879 for (String guess : guesses) { 880 if (guess.equalsIgnoreCase(tableName)) { 881 guessedTables.add(tableName); 882 } 883 } 884 } 885 if (guessedTables.size() == 0) { 886 throw new PersismException(Messages.CouldNotDetermineTableOrViewForType.message(isView ? "view" : "table", objectClass.getName(), guesses)); 887 } 888 889 if (guessedTables.size() > 1) { 890 throw new PersismException(Messages.CouldNotDetermineTableOrViewForTypeMultipleMatches.message(isView ? "view" : "table", objectClass.getName(), guesses, guessedTables)); 891 } 892 893 return guessedTables.get(0); 894 } 895 896 private void addTableGuesses(String className, Collection<String> guesses) { 897 // PascalCasing class name should make 898 // PascalCasing 899 // PascalCasings 900 // Pascal Casing 901 // Pascal Casings 902 // Pascal_Casing 903 // Pascal_Casings 904 // Order is important. 905 906 String guess; 907 String pluralClassName; 908 909 if (className.endsWith("y")) { 910 pluralClassName = className.substring(0, className.length() - 1) + "ies"; 911 } else { 912 pluralClassName = className + "s"; 913 } 914 915 guesses.add(className); 916 guesses.add(pluralClassName); 917 918 guess = camelToTitleCase(className); 919 guesses.add(guess); // name with spaces 920 guesses.add(replaceAll(guess, ' ', '_')); // name with spaces changed to _ 921 922 guess = camelToTitleCase(pluralClassName); 923 guesses.add(guess); // plural name with spaces 924 guesses.add(replaceAll(guess, ' ', '_')); // plural name with spaces changed to _ 925 } 926 927 List<String> getPrimaryKeys(Class<?> objectClass, Connection connection) throws PersismException { 928 929 // ensures meta data will be available 930 String tableName = getTableName(objectClass, connection); 931 932 List<String> primaryKeys = new ArrayList<>(4); 933 Map<String, ColumnInfo> map = getColumns(objectClass, connection); 934 for (ColumnInfo col : map.values()) { 935 if (col.primary) { 936 primaryKeys.add(col.columnName); 937 } 938 } 939 if (log.isDebugEnabled()) { 940 log.debug("getPrimaryKeys for %s %s", tableName, primaryKeys); 941 } 942 return primaryKeys; 943 } 944 945 ConnectionTypes getConnectionType() { 946 return connectionType; 947 } 948 949 }