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 }