Coverage Summary for Class: MetaData (net.sf.persism)

Class Class, % Method, % Line, %
MetaData 100% (1/1) 100% (37/37) 96.6% (588/609)


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 net.sf.persism.Util.*; 15  16 /** 17  * DB and POJO related Metadata collected based connection url 18  * 19  * @author Dan Howard 20  * @since 3/31/12 4:19 PM 21  */ 22 final class MetaData { 23  24  private static final Log log = Log.getLogger(MetaData.class); 25  26  // properties for each class - static because this won't need to change between MetaData instances (collection is unmodifiable) 27  private static final Map<Class<?>, Collection<PropertyInfo>> propertyMap = new ConcurrentHashMap<>(32); 28  29  // column to property map for each class 30  private final Map<Class<?>, Map<String, PropertyInfo>> propertyInfoMap = new ConcurrentHashMap<>(32); 31  private final Map<Class<?>, Map<String, ColumnInfo>> columnInfoMap = new ConcurrentHashMap<>(32); 32  33  // SQL for updates/inserts/deletes/selects for each class 34  private final Map<Class<?>, String> updateStatementsMap = new ConcurrentHashMap<>(32); 35  private final Map<Class<?>, String> deleteStatementsMap = new ConcurrentHashMap<>(32); 36  private final Map<Class<?>, String> selectStatementsMap = new ConcurrentHashMap<>(32); 37  38  // key - class, value - map key: columns to include, value: associated INSERT statement - this handles defaults on columns which may not need to be specified 39  private final Map<Class<?>, Map<String, String>> insertStatements = new ConcurrentHashMap<>(32); 40  41  // key - class, value - map key: changed columns, value: associated UPDATE statement 42  private final Map<Class<?>, Map<String, String>> variableUpdateStatements = new ConcurrentHashMap<>(32); 43  44  // Where clauses for primary key queries for tables 45  private final Map<Class<?>, String> primaryWhereClauseMap = new ConcurrentHashMap<>(32); 46  47  // Where ID IN (?, ?, ?) kinds of queries when no SQL is used. 48  private final Map<Class<?>, Map<Integer, String>> primaryInClauseMap = new ConcurrentHashMap<>(32); 49  50  // SQL parsed from SQL.where() - key is WHERE, value is parsed where clause 51  Map<Class<?>, Map<String, String>> whereClauses = new ConcurrentHashMap<>(32); 52  53  // WHERE clauses defined by JOIN operations (maintained by SessionHelper) 54  Map<JoinInfo, Map<String, String>> childWhereClauses = new ConcurrentHashMap<>(32); 55  56  // table/view for each class 57  private final Map<Class<?>, TableInfo> tableOrViewMap = new ConcurrentHashMap<>(32); 58  59  // list of tables in the DB 60  private final Set<TableInfo> tables = new HashSet<>(); 61  62  // list of views in the DB 63  private final Set<TableInfo> views = new HashSet<>(); 64  65  static final Map<String, MetaData> metaData = new ConcurrentHashMap<>(4); 66  67  private final ConnectionType connectionType; 68  69  // the "extra" characters that can be used in unquoted identifier names (those beyond a-z, A-Z, 0-9 and _) 70  // Was using DatabaseMetaData getExtraNameCharacters() but some drivers don't provide these and still allow 71  // for non-alphanumeric characters in column names. We'll just use a static set. 72  private static final String EXTRA_NAME_CHARACTERS = "`~!@#$%^&*()-+=/|\\{}[]:;'\".,<>*"; 73  private static final String SELECT_FOR_COLUMNS = "SELECT * FROM {0}{1}{2} WHERE 1=0"; 74  private static final String SELECT_FOR_COLUMNS_WITH_SCHEMA = "SELECT * FROM {0}{1}{2}.{3}{4}{5} WHERE 1=0"; 75  76  private MetaData(Connection con, String sessionKey) throws SQLException { 77  78  log.debug("MetaData CREATING instance [%s] ", sessionKey); 79  80  connectionType = ConnectionType.get(sessionKey); 81  if (connectionType == ConnectionType.Other) { 82  log.warn(Message.UnknownConnectionType.message(con.getMetaData().getDatabaseProductName())); 83  } 84  populateTableList(con); 85  } 86  87  static synchronized MetaData getInstance(Connection con, String sessionKey) throws SQLException { 88  89  if (sessionKey == null) { 90  sessionKey = con.getMetaData().getURL(); 91  } 92  93  if (metaData.get(sessionKey) == null) { 94  metaData.put(sessionKey, new MetaData(con, sessionKey)); 95  } 96  log.debug("MetaData getting instance %s", sessionKey); 97  return metaData.get(sessionKey); 98  } 99  100  // Should only be called IF the map does not contain the column meta information yet. 101  // Version for Tables 102  private synchronized <T> Map<String, PropertyInfo> determinePropertyInfo(Class<T> objectClass, TableInfo table, Connection connection) { 103  // double check map 104  if (propertyInfoMap.containsKey(objectClass)) { 105  return propertyInfoMap.get(objectClass); 106  } 107  108  // Not for @NotTable classes 109  assert objectClass.getAnnotation(NotTable.class) == null; 110  111  String sd = connectionType.getKeywordStartDelimiter(); 112  String ed = connectionType.getKeywordEndDelimiter(); 113  114  ResultSet rs = null; 115  Statement st = null; 116  try { 117  st = connection.createStatement(); 118  // gives us real column names with case. 119  String sql; 120  if (isEmpty(table.schema())) { 121  sql = MessageFormat.format(SELECT_FOR_COLUMNS, sd, table.name(), ed); 122  } else { 123  sql = MessageFormat.format(SELECT_FOR_COLUMNS_WITH_SCHEMA, sd, table.schema(), ed, sd, table.name(), ed); 124  } 125  if (log.isDebugEnabled()) { 126  log.debug("determineColumns: %s", sql); 127  } 128  rs = st.executeQuery(sql); 129  Map<String, PropertyInfo> columns = determinePropertyInfoFromResultSet(objectClass, rs); 130  propertyInfoMap.put(objectClass, columns); 131  return columns; 132  } catch (SQLException e) { 133  throw new PersismException(e.getMessage(), e); 134  } finally { 135  cleanup(st, rs); 136  } 137  } 138  139  private <T> Map<String, PropertyInfo> determinePropertyInfoFromResultSet(Class<T> objectClass, ResultSet rs) throws SQLException { 140  ResultSetMetaData rsmd = rs.getMetaData(); 141  Collection<PropertyInfo> properties = getPropertyInfo(objectClass); 142  143  int columnCount = rsmd.getColumnCount(); 144  145  Map<String, PropertyInfo> columns = new LinkedHashMap<>(columnCount); 146  for (int j = 1; j <= columnCount; j++) { 147  String realColumnName = rsmd.getColumnLabel(j); 148  String columnName = realColumnName.toLowerCase().replace("_", "").replace(" ", ""); 149  // also replace these characters 150  for (int x = 0; x < EXTRA_NAME_CHARACTERS.length(); x++) { 151  columnName = columnName.replace("" + EXTRA_NAME_CHARACTERS.charAt(x), ""); 152  } 153  PropertyInfo foundProperty = null; 154  for (PropertyInfo propertyInfo : properties) { 155  String checkName = propertyInfo.propertyName.toLowerCase().replace("_", ""); 156  if (checkName.equalsIgnoreCase(columnName)) { 157  foundProperty = propertyInfo; 158  break; 159  } else { 160  // check annotation against column name 161  Column column = (Column) propertyInfo.getAnnotation(Column.class); 162  if (column != null) { 163  if (column.name().equalsIgnoreCase(realColumnName)) { 164  foundProperty = propertyInfo; 165  break; 166  } 167  } 168  } 169  } 170  171  if (foundProperty != null) { 172  columns.put(realColumnName, foundProperty); 173  } else { 174  log.warn(Message.NoPropertyFoundForColumn.message(realColumnName, objectClass)); 175  } 176  } 177  return columns; 178  } 179  180  @SuppressWarnings({"JDBCExecuteWithNonConstantString", "SqlDialectInspection"}) 181  private synchronized <T> Map<String, ColumnInfo> determineColumnInfo(Class<T> objectClass, TableInfo table, Connection connection) { 182  if (columnInfoMap.containsKey(objectClass)) { 183  return columnInfoMap.get(objectClass); 184  } 185  186  Statement st = null; 187  ResultSet rs = null; 188  Map<String, PropertyInfo> properties = getTableColumnsPropertyInfo(objectClass, connection); 189  String sd = connectionType.getKeywordStartDelimiter(); 190  String ed = connectionType.getKeywordEndDelimiter(); 191  192  String schemaName = table.schema(); 193  String tableName = table.name(); 194  195  try { 196  st = connection.createStatement(); 197  String sql; 198  if (isEmpty(schemaName)) { 199  sql = MessageFormat.format(SELECT_FOR_COLUMNS, sd, tableName, ed); 200  } else { 201  sql = MessageFormat.format(SELECT_FOR_COLUMNS_WITH_SCHEMA, sd, schemaName, ed, sd, tableName, ed); 202  } 203  log.debug("determineColumnInfo %s", sql); 204  rs = st.executeQuery(sql); 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 = JavaType.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  if (!isDataClassATable(objectClass)) { 242  log.warn(Message.PrimaryAnnotationOnViewOrQueryMakesNoSense.message(objectClass, propertyInfo.propertyName)); 243  } 244  } 245  246  if (col.autoIncrement()) { 247  columnInfo.autoIncrement = true; 248  if (!columnInfo.columnType.isEligibleForAutoinc()) { 249  // This will probably cause some error or other problem. Notify the user. 250  log.warn(Message.ColumnAnnotatedAsAutoIncButNAN.message(columnInfo.columnName, columnInfo.columnType)); 251  } 252  } 253  254  columnInfo.readOnly = col.readOnly(); 255  256  if (!primaryKeysFound) { 257  primaryKeysFound = columnInfo.primary; 258  } 259  } 260  261  map.put(columnInfo.columnName, columnInfo); 262  } 263  } 264  rs.close(); 265  266  DatabaseMetaData dmd = connection.getMetaData(); 267  268  if (objectClass.getAnnotation(View.class) == null) { 269  270  if (isEmpty(schemaName)) { 271  rs = dmd.getPrimaryKeys(null, connectionType.getSchemaPattern(), tableName); 272  } else { 273  rs = dmd.getPrimaryKeys(null, schemaName, tableName); 274  } 275  int primaryKeysCount = 0; 276  while (rs.next()) { 277  ColumnInfo columnInfo = map.get(rs.getString("COLUMN_NAME")); 278  if (columnInfo != null) { 279  columnInfo.primary = true; 280  281  if (!primaryKeysFound) { 282  primaryKeysFound = columnInfo.primary; 283  } 284  } 285  primaryKeysCount++; 286  } 287  288  if (primaryKeysCount == 0 && !primaryKeysFound) { 289  log.warn(Message.DatabaseMetaDataCouldNotFindPrimaryKeys.message(table)); 290  } 291  } 292  293  /* 294  Get columns from database metadata since we don't get Type from resultSetMetaData 295  with SQLite. + We also need to know if there's a default on a column. 296  */ 297  if (isEmpty(schemaName)) { 298  rs = dmd.getColumns(null, connectionType.getSchemaPattern(), tableName, null); 299  } else { 300  rs = dmd.getColumns(null, schemaName, tableName, null); 301  } 302  int columnsCount = 0; 303  while (rs.next()) { 304  ColumnInfo columnInfo = map.get(rs.getString("COLUMN_NAME")); 305  if (columnInfo != null) { 306  if (!columnInfo.hasDefault) { 307  columnInfo.hasDefault = containsColumn(rs, "COLUMN_DEF") && rs.getString("COLUMN_DEF") != null; 308  } 309  310  // Do we not have autoinc info here? Yes. 311  // IS_AUTOINCREMENT = NO or YES 312  if (!columnInfo.autoIncrement) { 313  columnInfo.autoIncrement = containsColumn(rs, "IS_AUTOINCREMENT") && "YES".equalsIgnoreCase(rs.getString("IS_AUTOINCREMENT")); 314  } 315  316  // Re-assert the type since older version of SQLite could not detect types with empty resultsets 317  // It seems OK now in the newer JDBC driver. 318  // See testTypes unit test in TestSQLite 319  if (containsColumn(rs, "DATA_TYPE")) { 320  columnInfo.sqlColumnType = rs.getInt("DATA_TYPE"); 321  if (containsColumn(rs, "TYPE_NAME")) { 322  columnInfo.sqlColumnTypeName = rs.getString("TYPE_NAME"); 323  } 324  columnInfo.columnType = JavaType.convert(columnInfo.sqlColumnType); 325  } 326  } 327  columnsCount++; 328  } 329  rs.close(); 330  331  if (columnsCount == 0) { 332  // Shouldn't this be a fail? It would mean the user connecting to the DB 333  // has no permission to get the column meta-data 334  // It's a warning because it is possible to specify the column information 335  // with annotations rather than having Persism discover it. 336  log.warn(Message.DatabaseMetaDataCouldNotFindColumns.message(table)); 337  } 338  339  // FOR Oracle which doesn't set autoinc in metadata even if we have: 340  // "ID" NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY 341  // Apparently that's not enough for the Oracle JDBC driver to indicate this is autoinc. 342  // If we have a primary that's NUMERIC and HAS a default AND autoinc is not set then set it. 343  if (connectionType == ConnectionType.Oracle) { 344  Optional<ColumnInfo> autoInc = map.values().stream().filter(e -> e.autoIncrement).findFirst(); 345  if (autoInc.isEmpty()) { 346  // Do a second check if we have a primary that's numeric with a default. 347  Optional<ColumnInfo> primaryOpt = map.values().stream().filter(e -> e.primary).findFirst(); 348  if (primaryOpt.isPresent()) { 349  ColumnInfo primary = primaryOpt.get(); 350  if (primary.columnType.isEligibleForAutoinc() && primary.hasDefault) { 351  primary.autoIncrement = true; 352  primaryKeysFound = true; 353  } 354  } 355  } 356  } 357  358  if (!primaryKeysFound && isDataClassATable(objectClass)) { 359  // Should we fail-fast? Actually no, we should not fail here. 360  // It's very possible the user has a table that they will never 361  // update, delete or select (by primary). 362  // They may only want to do read operations with specified queries and in that 363  // context we don't need any primary keys. (same with insert) 364  log.warn(Message.NoPrimaryKeyFoundForTable.message(table)); 365  } 366  367  columnInfoMap.put(objectClass, map); 368  return map; 369  370  } catch (SQLException e) { 371  throw new PersismException(e.getMessage(), e); 372  } finally { 373  cleanup(st, rs); 374  } 375  } 376  377  private static boolean isDataClassATable(Class<?> objectClass) { 378  return objectClass.getAnnotation(Table.class) != null || (objectClass.getAnnotation(View.class) == null && objectClass.getAnnotation(NotTable.class) == null); 379  } 380  381  static <T> Collection<PropertyInfo> getPropertyInfo(Class<T> objectClass) { 382  if (propertyMap.containsKey(objectClass)) { 383  return propertyMap.get(objectClass); 384  } 385  return determinePropertyInfo(objectClass); 386  } 387  388  private static synchronized <T> Collection<PropertyInfo> determinePropertyInfo(Class<T> objectClass) { 389  if (propertyMap.containsKey(objectClass)) { 390  return propertyMap.get(objectClass); 391  } 392  393  Map<String, PropertyInfo> propertyInfos = new HashMap<>(32); 394  395  List<Field> fields = new ArrayList<>(32); 396  397  // getDeclaredFields does not get fields from super classes..... 398  fields.addAll(Arrays.asList(objectClass.getDeclaredFields())); 399  Class<?> sup = objectClass.getSuperclass(); 400  log.debug("fields for %s", sup); 401  while (!sup.equals(Object.class) && !sup.equals(PersistableObject.class)) { 402  fields.addAll(Arrays.asList(sup.getDeclaredFields())); 403  sup = sup.getSuperclass(); 404  log.debug("fields for %s", sup); 405  } 406  407  Method[] methods = objectClass.getMethods(); 408  409  for (Field field : fields) { 410  // Skip static fields 411  if (Modifier.isStatic(field.getModifiers())) { 412  continue; 413  } 414 // log.debug("Field Name: %s", field.getName()); 415  String propertyName = field.getName(); 416 // log.debug("Property Name: *%s* ", propertyName); 417  418  PropertyInfo propertyInfo = new PropertyInfo(); 419  propertyInfo.propertyName = propertyName; 420  propertyInfo.field = field; 421  Annotation[] annotations = field.getAnnotations(); 422  for (Annotation annotation : annotations) { 423  propertyInfo.annotations.put(annotation.annotationType(), annotation); 424  } 425  426  for (Method method : methods) { 427  String propertyNameToTest = field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1); 428  // log.debug("property name for testing %s", propertyNameToTest); 429  if (propertyNameToTest.startsWith("Is") && propertyNameToTest.length() > 2 && Character.isUpperCase(propertyNameToTest.charAt(2))) { 430  propertyNameToTest = propertyName.substring(2); 431  } 432  433  String[] candidates = {"set" + propertyNameToTest, "get" + propertyNameToTest, "is" + propertyNameToTest, field.getName()}; 434  435  if (Arrays.asList(candidates).contains(method.getName())) { 436  //log.debug(" METHOD: %s", method.getName()); 437  438  annotations = method.getAnnotations(); 439  for (Annotation annotation : annotations) { 440  propertyInfo.annotations.put(annotation.annotationType(), annotation); 441  } 442  443  // OR added to fix to builder pattern style when your setters are just the field name 444  if (method.getName().equalsIgnoreCase("set" + propertyNameToTest) || method.getParameterCount() > 0) { 445  propertyInfo.setter = method; 446  } else { 447  propertyInfo.getter = method; 448  } 449  } 450  } 451  452  propertyInfo.isJoin = propertyInfo.getAnnotation(Join.class) != null; 453  propertyInfos.put(propertyName.toLowerCase(), propertyInfo); 454  } 455  456  // Remove any properties found with the NotColumn annotation 457  // http://stackoverflow.com/questions/2026104/hashmap-keyset-foreach-and-remove 458  Iterator<Map.Entry<String, PropertyInfo>> it = propertyInfos.entrySet().iterator(); 459  while (it.hasNext()) { 460  Map.Entry<String, PropertyInfo> entry = it.next(); 461  PropertyInfo info = entry.getValue(); 462  // added support for transient 463  if (info.getAnnotation(NotColumn.class) != null || Modifier.isTransient(info.field.getModifiers())) { 464  it.remove(); 465  } 466  } 467  468  Collection<PropertyInfo> properties = Collections.unmodifiableCollection(propertyInfos.values()); 469  propertyMap.put(objectClass, properties); 470  471  // If a view or query - warn if we find any setters 472  if (objectClass.getAnnotation(NotTable.class) != null || objectClass.getAnnotation(View.class) != null) { 473  List<String> setters = new ArrayList<>(); 474  for (PropertyInfo propertyInfo : properties) { 475  if (propertyInfo.setter != null) { 476  setters.add(propertyInfo.propertyName); 477  } 478  } 479  480  if (setters.size() > 0) { 481  log.warn(Message.SettersFoundInReadOnlyObject.message(objectClass, setters)); 482  } 483  } 484  485  return properties; 486  } 487  488  private static final String TABLE = "TABLE"; 489  private static final String VIEW = "VIEW"; 490  private static final String[] tableTypes = {TABLE, VIEW}; 491  492  // Populates the tables list with table names from the DB. 493  // This list is used for discovery of the table name from a class. 494  // ONLY to be called from Init in a synchronized way. 495  // NULL POINTER WITH 496  // http://social.msdn.microsoft.com/Forums/en-US/sqldataaccess/thread/5c74094a-8506-4278-ac1c-f07d1bfdb266 497  // solution: 498  // http://stackoverflow.com/questions/8988945/java7-sqljdbc4-sql-error-08s01-on-getconnection 499  private void populateTableList(Connection con) throws PersismException { 500  try (ResultSet rs = con.getMetaData().getTables(null, connectionType.getSchemaPattern(), null, tableTypes)) { 501  String name; 502  while (rs.next()) { 503  name = rs.getString("TABLE_NAME"); 504  if (VIEW.equalsIgnoreCase(rs.getString("TABLE_TYPE"))) { 505  views.add(new TableInfo(name, rs.getString("TABLE_SCHEM"), connectionType)); 506  } else { 507  tables.add(new TableInfo(name, rs.getString("TABLE_SCHEM"), connectionType)); 508  } 509  } 510  } catch (SQLException e) { 511  throw new PersismException(e.getMessage(), e); 512  } 513  } 514  515  /** 516  * @param object 517  * @param connection 518  * @return sql update string 519  * @throws NoChangesDetectedForUpdateException if the data object implements Persistable and there are no changes detected 520  */ 521  String getUpdateStatement(Object object, Connection connection) throws PersismException, NoChangesDetectedForUpdateException { 522  523  String sql; 524  if (object instanceof Persistable<?> pojo) { 525  Map<String, PropertyInfo> changes = getChangedProperties(pojo, connection); 526  if (changes.size() == 0) { 527  throw new NoChangesDetectedForUpdateException(); 528  } 529  530  Class<?> objectClass = object.getClass(); 531  String key = changes.keySet().toString(); 532  if (variableUpdateStatements.containsKey(objectClass) && variableUpdateStatements.get(objectClass).containsKey(key)) { 533  sql = variableUpdateStatements.get(objectClass).get(key); 534  } else { 535  sql = determineUpdateStatement(pojo, connection); 536  } 537  538  if (log.isDebugEnabled()) { 539  log.debug("getUpdateStatement for %s for changed fields is %s", objectClass, sql); 540  } 541  return sql; 542  } 543  544  if (updateStatementsMap.containsKey(object.getClass())) { 545  sql = updateStatementsMap.get(object.getClass()); 546  } else { 547  sql = determineUpdateStatement(object, connection); 548  } 549  if (log.isDebugEnabled()) { 550  log.debug("getUpdateStatement for: %s %s", object.getClass(), sql); 551  } 552  return sql; 553  } 554  555  // Used by Objects not implementing Persistable since they will always use the same update statement 556  private synchronized String determineUpdateStatement(Object object, Connection connection) { 557  if (updateStatementsMap.containsKey(object.getClass())) { 558  return updateStatementsMap.get(object.getClass()); 559  } 560  561  Class<?> objectClass = object.getClass(); 562  var columns = getColumns(objectClass, connection); 563  Map<String, PropertyInfo> propertyMap; 564  if (object instanceof Persistable<?> pojo) { 565  propertyMap = getChangedProperties(pojo, connection); 566  } else { 567  propertyMap = getTableColumnsPropertyInfo(objectClass, connection); 568  } 569  570  String updateStatement = buildUpdateString(object, propertyMap.keySet().stream().filter(col -> !columns.get(col).readOnly).toList().iterator(), connection); 571  572  if (object instanceof Persistable<?>) { 573  String key = propertyMap.keySet().toString(); 574  if (variableUpdateStatements.containsKey(objectClass) && variableUpdateStatements.get(objectClass).containsKey(key)) { 575  return variableUpdateStatements.get(objectClass).get(key); 576  } 577  578  variableUpdateStatements.putIfAbsent(objectClass, new HashMap<>()); 579  variableUpdateStatements.get(objectClass).put(key, updateStatement); 580  } else { 581  updateStatementsMap.put(objectClass, updateStatement); 582  } 583  584  585  if (log.isDebugEnabled()) { 586  log.debug("determineUpdateStatement for %s is %s", objectClass, updateStatement); 587  } 588  589  return updateStatement; 590  } 591  592  String getInsertStatement(Object object, Connection connection) throws PersismException { 593  String sql; 594  String key = getColumnsForInsert(object, connection).stream().map(columnInfo -> columnInfo.columnName).toList().toString(); 595  Class<?> objectClass = object.getClass(); 596  if (insertStatements.containsKey(objectClass) && insertStatements.get(objectClass).containsKey(key)) { 597  sql = insertStatements.get(objectClass).get(key); 598  } else { 599  sql = determineInsertStatement(object, connection); 600  } 601  if (log.isDebugEnabled()) { 602  log.debug("getInsertStatement for: %s %s", objectClass, sql); 603  } 604  return sql; 605  } 606  607  private synchronized String determineInsertStatement(Object object, Connection connection) { 608  List<ColumnInfo> columnsForInsert = getColumnsForInsert(object, connection); 609  String key = columnsForInsert.stream().map(columnInfo -> columnInfo.columnName).toList().toString(); 610  Class<?> objectClass = object.getClass(); 611  612  if (insertStatements.containsKey(objectClass) && insertStatements.get(objectClass).containsKey(key)) { 613  return insertStatements.get(objectClass).get(key); 614  } 615  616  String sd = connectionType.getKeywordStartDelimiter(); 617  String ed = connectionType.getKeywordEndDelimiter(); 618  619  TableInfo tableInfo = getTableInfo(objectClass); 620  String tableName = tableInfo.name(); 621  String schemaName = tableInfo.schema(); 622  623  StringBuilder sbi = new StringBuilder(); 624  sbi.append("INSERT INTO "); 625  if (isNotEmpty(schemaName)) { 626  sbi.append(sd).append(schemaName).append(ed).append("."); 627  } 628  sbi.append(sd).append(tableName).append(ed).append(" ("); 629  630  StringBuilder sbp = new StringBuilder(); 631  sbp.append(" VALUES ("); 632  633  String sep = ""; 634  635  for (ColumnInfo column : columnsForInsert) { 636  sbi.append(sep).append(sd).append(column.columnName).append(ed); 637  sbp.append(sep).append("?"); 638  sep = ", "; 639  } 640  641  if (connectionType == ConnectionType.MSSQL) { 642  // check if we have a sequence or other non auto-inc primary and use OUTPUT inserted.[COL] to get the value back. 643  Optional<ColumnInfo> primary = getPrimaryNonAutoIncColumn(object, connection); 644  if (primary.isPresent()) { 645  ColumnInfo col = primary.get(); 646  sbi.append(") ").append("OUTPUT inserted.").append(sd).append(col.columnName).append(ed).append(sbp).append(") "); 647  } else { 648  sbi.append(") ").append(sbp).append(") "); 649  } 650  } else { 651  sbi.append(") ").append(sbp).append(") "); 652  } 653  654  String insertStatement; 655  insertStatement = sbi.toString(); 656  657  if (log.isDebugEnabled()) { 658  log.debug("determineInsertStatement for %s is %s", object.getClass(), insertStatement); 659  } 660  661  insertStatements.putIfAbsent(objectClass, new HashMap<>()); 662  insertStatements.get(objectClass).put(key, insertStatement); 663  664  return insertStatement; 665  } 666  667  private List<ColumnInfo> getColumnsForInsert(Object object, Connection connection) { 668  Map<String, ColumnInfo> columns = getColumns(object.getClass(), connection); 669  Map<String, PropertyInfo> properties = getTableColumnsPropertyInfo(object.getClass(), connection); 670  return columns.values().stream(). 671  filter(col -> !col.autoIncrement). 672  filter(col -> !(col.hasDefault && properties.get(col.columnName).getValue(object) == null)). 673  filter(col -> !col.readOnly). 674  toList(); 675  } 676  677  private Optional<ColumnInfo> getPrimaryNonAutoIncColumn(Object object, Connection connection) { 678  Map<String, ColumnInfo> columns = getColumns(object.getClass(), connection); 679  Map<String, PropertyInfo> properties = getTableColumnsPropertyInfo(object.getClass(), connection); 680  return columns.values().stream(). 681  filter(col -> col.primary && col.hasDefault && properties.get(col.columnName).getValue(object) == null). 682  findFirst(); 683  } 684  685  String getDeleteStatement(Class<?> objectClass, Connection connection) { 686  if (deleteStatementsMap.containsKey(objectClass)) { 687  return deleteStatementsMap.get(objectClass); 688  } 689  return determineDeleteStatement(objectClass, connection); 690  } 691  692  String getDefaultDeleteStatement(Class<?> objectClass, Connection connection) { 693  return getDeleteStatement(objectClass, connection) + getWhereClause(objectClass, connection); 694  } 695  696  private synchronized String determineDeleteStatement(Class<?> objectClass, Connection connection) { 697  if (deleteStatementsMap.containsKey(objectClass)) { 698  return deleteStatementsMap.get(objectClass); 699  } 700  701  String sd = connectionType.getKeywordStartDelimiter(); 702  String ed = connectionType.getKeywordEndDelimiter(); 703  704  TableInfo tableInfo = getTableInfo(objectClass); 705  String tableName = tableInfo.name(); 706  String schemaName = tableInfo.schema(); 707  708  StringBuilder sb = new StringBuilder(); 709  sb.append("DELETE FROM "); 710  if (isNotEmpty(schemaName)) { 711  sb.append(sd).append(schemaName).append(ed).append("."); 712  } 713  sb.append(sd).append(tableName).append(ed); 714  String deleteStatement = sb.toString(); 715  716  if (log.isDebugEnabled()) { 717  log.debug("determineDeleteStatement for %s is %s", objectClass, deleteStatement); 718  } 719  720  deleteStatementsMap.put(objectClass, deleteStatement); 721  return deleteStatement; 722  } 723  724  String getPrimaryInClause(Class<?> objectClass, int paramCount, Connection connection) { 725  if (primaryInClauseMap.containsKey(objectClass) && primaryInClauseMap.get(objectClass).containsKey(paramCount)) { 726  return primaryInClauseMap.get(objectClass).get(paramCount); 727  } 728  return determinePrimaryInClause(objectClass, paramCount, connection); 729  } 730  731  private synchronized String determinePrimaryInClause(Class<?> objectClass, int paramCount, Connection connection) { 732  if (primaryInClauseMap.containsKey(objectClass) && primaryInClauseMap.get(objectClass).containsKey(paramCount)) { 733  return primaryInClauseMap.get(objectClass).get(paramCount); 734  } 735  736  Map<Integer, String> map = primaryInClauseMap.get(objectClass); 737  if (map == null) { 738  map = new HashMap<>(); 739  } 740  741  String sd = connectionType.getKeywordStartDelimiter(); 742  String ed = connectionType.getKeywordEndDelimiter(); 743  String andSep = ""; 744  745  String query = " WHERE "; 746  747  List<String> primaryKeys = getPrimaryKeys(objectClass, connection); 748  749  StringBuilder sb = new StringBuilder(query); 750  int groups = paramCount / primaryKeys.size(); 751  for (String column : primaryKeys) { 752  String sep = ""; 753  sb.append(andSep).append(sd).append(column).append(ed).append(" IN ("); 754  for (int j = 0; j < groups; j++) { 755  sb.append(sep).append("?"); 756  sep = ", "; 757  } 758  sb.append(")"); 759  andSep = " AND "; 760  } 761  query = sb.toString(); 762  763  map.put(paramCount, query); 764  primaryInClauseMap.put(objectClass, map); 765  766  return query; 767  } 768  769  770  String getWhereClause(Class<?> objectClass, Connection connection) { 771  if (primaryWhereClauseMap.containsKey(objectClass)) { 772  return primaryWhereClauseMap.get(objectClass); 773  } 774  return determineWhereClause(objectClass, connection); 775  } 776  777  private synchronized String determineWhereClause(Class<?> objectClass, Connection connection) { 778  if (primaryWhereClauseMap.containsKey(objectClass)) { 779  return primaryWhereClauseMap.get(objectClass); 780  } 781  782  String sep = ""; 783  784  StringBuilder sb = new StringBuilder(); 785  String sd = connectionType.getKeywordStartDelimiter(); 786  String ed = connectionType.getKeywordEndDelimiter(); 787  788  List<String> primaryKeys = getPrimaryKeys(objectClass, connection); 789  if (primaryKeys.size() == 0) { 790  throw new PersismException(Message.TableHasNoPrimaryKeysForWhere.message(getTableInfo(objectClass).name())); 791  } 792  793  sb.append(" WHERE "); 794  795  sep = ""; 796  for (String column : primaryKeys) { 797  sb.append(sep).append(sd).append(column).append(ed).append(" = ?"); 798  sep = " AND "; 799  } 800  801  String where = sb.toString(); 802  if (log.isDebugEnabled()) { 803  log.debug("determineWhereClause: %s %s", objectClass.getName(), where); 804  } 805  primaryWhereClauseMap.put(objectClass, where); 806  return where; 807  } 808  809  /* 810  * Default SELECT including WHERE Primary Keys - should only be called for tables 811  */ 812  String getDefaultSelectStatement(Class<?> objectClass, Connection connection) { 813  assert objectClass.getAnnotation(View.class) == null; 814  assert objectClass.getAnnotation(NotTable.class) == null; 815  816  return getSelectStatement(objectClass, connection) + getWhereClause(objectClass, connection); 817  } 818  819  /** 820  * SQL SELECT COLUMNS ONLY - make public? or put a delegate somewhere else? 821  * 822  * @param objectClass 823  * @param connection 824  * @return 825  */ 826  String getSelectStatement(Class<?> objectClass, Connection connection) { 827  if (selectStatementsMap.containsKey(objectClass)) { 828  return selectStatementsMap.get(objectClass); 829  } 830  return determineSelectStatement(objectClass, connection); 831  } 832  833  private synchronized String determineSelectStatement(Class<?> objectClass, Connection connection) { 834  835  if (selectStatementsMap.containsKey(objectClass)) { 836  return selectStatementsMap.get(objectClass); 837  } 838  839  String sd = connectionType.getKeywordStartDelimiter(); 840  String ed = connectionType.getKeywordEndDelimiter(); 841  842  TableInfo tableInfo = getTableInfo(objectClass); 843  String tableName = tableInfo.name(); 844  String schemaName = tableInfo.schema(); 845  846  StringBuilder sb = new StringBuilder(); 847  sb.append("SELECT "); 848  849  String sep = ""; 850  851  Map<String, ColumnInfo> columns = getColumns(objectClass, connection); 852  for (String column : columns.keySet()) { 853  ColumnInfo columnInfo = columns.get(column); 854  sb.append(sep).append(sd).append(columnInfo.columnName).append(ed); 855  sep = ", "; 856  } 857  sb.append(" FROM "); 858  if (isNotEmpty(schemaName)) { 859  sb.append(sd).append(schemaName).append(ed).append('.'); 860  } 861  sb.append(sd).append(tableName).append(ed); 862  863  864  String selectStatement = sb.toString(); 865  866  if (log.isDebugEnabled()) { 867  log.debug("determineSelectStatement for %s is %s", objectClass, selectStatement); 868  } 869  870  selectStatementsMap.put(objectClass, selectStatement); 871  872  return selectStatement; 873  } 874  875  private String buildUpdateString(Object object, Iterator<String> it, Connection connection) throws PersismException { 876  String sd = connectionType.getKeywordStartDelimiter(); 877  String ed = connectionType.getKeywordEndDelimiter(); 878  879  TableInfo tableInfo = getTableInfo(object.getClass()); 880  String tableName = tableInfo.name(); 881  String schemaName = tableInfo.schema(); 882  883  List<String> primaryKeys = getPrimaryKeys(object.getClass(), connection); 884  885  StringBuilder sb = new StringBuilder(); 886  sb.append("UPDATE "); 887  if (isNotEmpty(schemaName)) { 888  sb.append(sd).append(schemaName).append(ed).append("."); 889  } 890  sb.append(sd).append(tableName).append(ed).append(" SET "); 891  String sep = ""; 892  893  Map<String, ColumnInfo> columns = getColumns(object.getClass(), connection); 894  while (it.hasNext()) { 895  String column = it.next(); 896  ColumnInfo columnInfo = columns.get(column); 897  if (columnInfo.autoIncrement || columnInfo.primary) { 898  log.debug("buildUpdateString: skipping " + column); 899  } else { 900  sb.append(sep).append(sd).append(column).append(ed).append(" = ?"); 901  sep = ", "; 902  } 903  } 904  sb.append(" WHERE "); 905  sep = ""; 906  for (String column : primaryKeys) { 907  sb.append(sep).append(sd).append(column).append(ed).append(" = ?"); 908  sep = " AND "; 909  } 910  return sb.toString(); 911  } 912  913  Map<String, PropertyInfo> getChangedProperties(Persistable<?> persistable, Connection connection) throws PersismException { 914  915  Persistable<?> original = (Persistable<?>) persistable.readOriginalValue(); 916  917  Map<String, PropertyInfo> columns = getTableColumnsPropertyInfo(persistable.getClass(), connection); 918  919  if (original == null) { 920  // Could happen in the case of cloning or other operation - so it's never read, so it never sets original. 921  return columns; 922  } else { 923  Map<String, PropertyInfo> changedColumns = new LinkedHashMap<>(columns.keySet().size()); 924  925  for (String column : columns.keySet()) { 926  PropertyInfo propertyInfo = columns.get(column); 927  928  Object newValue = propertyInfo.getValue(persistable); 929  Object orgValue = propertyInfo.getValue(original); 930  if (!Objects.equals(newValue, orgValue)) { 931  changedColumns.put(column, propertyInfo); 932  } 933  } 934  return changedColumns; 935  } 936  937  } 938  939  <T> Map<String, ColumnInfo> getColumns(Class<T> objectClass, Connection connection) throws PersismException { 940  // Realistically at this point this objectClass will always be in the map since it's defined early 941  // when we get the table name, but I'll double-check it for determineColumnInfo anyway. 942  if (columnInfoMap.containsKey(objectClass)) { 943  return columnInfoMap.get(objectClass); 944  } 945  return determineColumnInfo(objectClass, getTableInfo(objectClass), connection); 946  } 947  948  <T> Map<String, PropertyInfo> getQueryColumnsPropertyInfo(Class<T> objectClass, ResultSet rs) throws PersismException { 949  try { 950  return determinePropertyInfoFromResultSet(objectClass, rs); 951  } catch (SQLException e) { 952  throw new PersismException(e.getMessage(), e); 953  } 954  } 955  956  <T> Map<String, PropertyInfo> getTableColumnsPropertyInfo(Class<T> objectClass, Connection connection) throws PersismException { 957  if (propertyInfoMap.containsKey(objectClass)) { 958  return propertyInfoMap.get(objectClass); 959  } 960  return determinePropertyInfo(objectClass, getTableInfo(objectClass), connection); 961  } 962  963  <T> TableInfo getTableInfo(Class<T> objectClass) { 964  if (tableOrViewMap.containsKey(objectClass)) { 965  return tableOrViewMap.get(objectClass); 966  } 967  968  return determineTableInfo(objectClass); 969  } 970  971  private synchronized <T> TableInfo determineTableInfo(Class<T> objectClass) { 972  if (tableOrViewMap.containsKey(objectClass)) { 973  return tableOrViewMap.get(objectClass); 974  } 975  976  String tableName; 977  String schemaName = null; 978  TableInfo foundInfo = null; 979  980  Table tableAnnotation = objectClass.getAnnotation(Table.class); 981  View viewAnnotation = objectClass.getAnnotation(View.class); 982  983  if (tableAnnotation != null) { 984  tableName = tableAnnotation.value(); 985  if (tableName.contains(".")) { 986  schemaName = tableName.substring(0, tableName.indexOf(".")); 987  tableName = tableName.substring(tableName.indexOf(".") + 1); 988  } 989  990  boolean found = false; 991  for (TableInfo table : tables) { 992  if (table.name().equalsIgnoreCase(tableName)) { 993  if (schemaName != null) { 994  if (table.schema().equalsIgnoreCase(schemaName)) { 995  foundInfo = table; 996  found = true; 997  break; 998  } 999  } else { 1000  foundInfo = table; 1001  found = true; 1002  break; 1003  } 1004  } 1005  } 1006  if (schemaName == null) { 1007  final String table = tableName; 1008  if (tables.stream().filter(tableInfo -> tableInfo.name().equalsIgnoreCase(table)).count() > 1) { 1009  throw new PersismException(Message.MoreThanOneTableOrViewInDifferentSchemas.message("TABLE", table)); 1010  } 1011  } 1012  if (!found) { 1013  throw new PersismException(Message.CouldNotFindTableNameInTheDatabase.message(tableName, objectClass.getName())); 1014  } 1015  } else if (viewAnnotation != null && isNotEmpty(viewAnnotation.value())) { 1016  tableName = viewAnnotation.value(); 1017  if (tableName.contains(".")) { 1018  schemaName = tableName.substring(0, tableName.indexOf(".")); 1019  tableName = tableName.substring(tableName.indexOf(".") + 1); 1020  } 1021  1022  boolean found = false; 1023  for (TableInfo view : views) { 1024  if (view.name().equalsIgnoreCase(tableName)) { 1025  if (schemaName != null) { 1026  if (view.schema().equalsIgnoreCase(schemaName)) { 1027  foundInfo = view; 1028  found = true; 1029  break; 1030  } 1031  } else { 1032  foundInfo = view; 1033  found = true; 1034  break; 1035  } 1036  } 1037  } 1038  if (schemaName == null) { 1039  final String table = tableName; 1040  if (views.stream().filter(tableInfo -> tableInfo.name().equalsIgnoreCase(table)).count() > 1) { 1041  throw new PersismException(Message.MoreThanOneTableOrViewInDifferentSchemas.message("VIEW", table)); 1042  } 1043  } 1044  if (!found) { 1045  throw new PersismException(Message.CouldNotFindViewNameInTheDatabase.message(tableName, objectClass.getName())); 1046  } 1047  } else { 1048  foundInfo = guessTableOrView(objectClass); 1049  } 1050  tableOrViewMap.put(objectClass, foundInfo); 1051  return foundInfo; 1052  } 1053  1054  // Returns the table/view name found in the DB in the same case as in the DB. 1055  // throws PersismException if we cannot guess any table/view name for this class. 1056  private <T> TableInfo guessTableOrView(Class<T> objectClass) throws PersismException { 1057  Set<String> guesses = new LinkedHashSet<>(6); // guess order is important 1058  List<TableInfo> guessedTables = new ArrayList<>(6); 1059  1060  String className = objectClass.getSimpleName(); 1061  1062  Set<TableInfo> list; 1063  boolean isView = false; 1064  if (objectClass.getAnnotation(View.class) != null) { 1065  list = views; 1066  isView = true; 1067  } else { 1068  list = tables; 1069  } 1070  1071  addTableGuesses(className, guesses); 1072  for (TableInfo table : list) { 1073  for (String guess : guesses) { 1074  if (guess.equalsIgnoreCase(table.name())) { 1075  guessedTables.add(table); 1076  } 1077  } 1078  } 1079  if (guessedTables.size() == 0) { 1080  throw new PersismException(Message.CouldNotDetermineTableOrViewForType.message(isView ? "view" : "table", objectClass.getName(), guesses)); 1081  } 1082  1083  if (guessedTables.size() > 1) { 1084  Set<String> multipleGuesses = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); 1085  multipleGuesses.addAll(guessedTables.stream().map(TableInfo::name).toList()); 1086  throw new PersismException(Message.CouldNotDetermineTableOrViewForTypeMultipleMatches.message(isView ? "view" : "table", objectClass.getName(), guesses, multipleGuesses)); 1087  } 1088  return guessedTables.get(0); 1089  } 1090  1091  private void addTableGuesses(String className, Collection<String> guesses) { 1092  // PascalCasing class name should make 1093  // PascalCasing 1094  // PascalCasings 1095  // Pascal Casing 1096  // Pascal Casings 1097  // Pascal_Casing 1098  // Pascal_Casings 1099  // Order is important. 1100  1101  String guess; 1102  String pluralClassName; 1103  String pluralClassName2 = null; 1104  1105  if (className.endsWith("y")) { 1106  // supply - supplies, category - categories 1107  pluralClassName = className.substring(0, className.length() - 1) + "ies"; 1108  pluralClassName2 = className + "s"; // holiday 1109  } else if (className.endsWith("x")) { 1110  // tax - taxes, mailbox - mailboxes 1111  pluralClassName = className + "es"; 1112  } else { 1113  pluralClassName = className + "s"; 1114  } 1115  1116  guesses.add(className); 1117  guesses.add(pluralClassName); 1118  if (pluralClassName2 != null) { 1119  guesses.add(pluralClassName2); 1120  } 1121  1122  guess = camelToTitleCase(className); 1123  guesses.add(guess); // name with spaces 1124  guesses.add(guess.replaceAll(" ", "_")); // name with spaces changed to _ 1125  1126  guess = camelToTitleCase(pluralClassName); 1127  guesses.add(guess); // plural name with spaces 1128  guesses.add(guess.replaceAll(" ", "_")); // plural name with spaces changed to _ 1129  1130  if (pluralClassName2 != null) { 1131  guess = camelToTitleCase(pluralClassName2); 1132  guesses.add(guess); // plural name with spaces 1133  guesses.add(guess.replaceAll(" ", "_")); // plural name with spaces changed to _ 1134  } 1135  } 1136  1137  List<String> getPrimaryKeys(Class<?> objectClass, Connection connection) throws PersismException { 1138  // todo cache? called by Session and SessionHelper. 1139  // ensures meta-data will be available because this method could be called before getting the table info object 1140  TableInfo tableInfo = getTableInfo(objectClass); 1141  1142  List<String> primaryKeys = new ArrayList<>(4); 1143  Map<String, ColumnInfo> map = getColumns(objectClass, connection); 1144  for (ColumnInfo col : map.values()) { 1145  if (col.primary) { 1146  primaryKeys.add(col.columnName); 1147  } 1148  } 1149  if (log.isDebugEnabled()) { 1150  log.debug("getPrimaryKeys for %s %s", tableInfo.name(), primaryKeys); 1151  } 1152  return primaryKeys; 1153  } 1154  1155  ConnectionType getConnectionType() { 1156  return connectionType; 1157  } 1158  1159 }