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 }