001/* 002 * Copyright 2015-2022 Transmogrify LLC, 2022-2024 Revetware LLC. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package com.pyranid; 018 019import javax.annotation.Nonnull; 020import javax.annotation.Nullable; 021import javax.annotation.concurrent.ThreadSafe; 022import java.beans.BeanInfo; 023import java.beans.Introspector; 024import java.beans.PropertyDescriptor; 025import java.lang.reflect.Field; 026import java.lang.reflect.Method; 027import java.lang.reflect.Parameter; 028import java.lang.reflect.RecordComponent; 029import java.math.BigDecimal; 030import java.math.BigInteger; 031import java.sql.ResultSet; 032import java.sql.ResultSetMetaData; 033import java.sql.SQLException; 034import java.sql.Timestamp; 035import java.time.Instant; 036import java.time.LocalDate; 037import java.time.LocalDateTime; 038import java.time.LocalTime; 039import java.time.OffsetDateTime; 040import java.time.OffsetTime; 041import java.time.ZoneId; 042import java.util.ArrayList; 043import java.util.Calendar; 044import java.util.Currency; 045import java.util.Date; 046import java.util.HashMap; 047import java.util.HashSet; 048import java.util.List; 049import java.util.Locale; 050import java.util.Map; 051import java.util.Optional; 052import java.util.Set; 053import java.util.TimeZone; 054import java.util.UUID; 055import java.util.concurrent.ConcurrentHashMap; 056 057import static java.lang.String.format; 058import static java.util.Arrays.asList; 059import static java.util.Collections.unmodifiableMap; 060import static java.util.Collections.unmodifiableSet; 061import static java.util.Locale.ENGLISH; 062import static java.util.Objects.requireNonNull; 063import static java.util.stream.Collectors.joining; 064import static java.util.stream.Collectors.toSet; 065 066/** 067 * Basic implementation of {@link ResultSetMapper}. 068 * 069 * @author <a href="https://www.revetkn.com">Mark Allen</a> 070 * @since 1.0.0 071 */ 072@ThreadSafe 073public class DefaultResultSetMapper implements ResultSetMapper { 074 @Nonnull 075 private final DatabaseType databaseType; 076 @Nonnull 077 private final ZoneId timeZone; 078 @Nonnull 079 private final Calendar timeZoneCalendar; 080 @Nonnull 081 private final Map<Class<?>, Map<String, Set<String>>> columnLabelAliasesByPropertyNameCache = 082 new ConcurrentHashMap<>(); 083 084 /** 085 * Creates a {@code ResultSetMapper} with default configuration. 086 */ 087 public DefaultResultSetMapper() { 088 this(null, null); 089 } 090 091 /** 092 * Creates a {@code ResultSetMapper} for the given {@code databaseType}. 093 * 094 * @param databaseType the type of database we're working with 095 */ 096 public DefaultResultSetMapper(@Nullable DatabaseType databaseType) { 097 this(databaseType, null); 098 } 099 100 /** 101 * Creates a {@code ResultSetMapper} for the given {@code timeZone}. 102 * 103 * @param timeZone the timezone to use when working with {@link java.sql.Timestamp} and similar values 104 */ 105 public DefaultResultSetMapper(@Nullable ZoneId timeZone) { 106 this(null, timeZone); 107 } 108 109 /** 110 * Creates a {@code ResultSetMapper} for the given {@code databaseType} and {@code timeZone}. 111 * 112 * @param databaseType the type of database we're working with 113 * @param timeZone the timezone to use when working with {@link java.sql.Timestamp} and similar values 114 * @since 1.0.15 115 */ 116 public DefaultResultSetMapper(@Nullable DatabaseType databaseType, 117 @Nullable ZoneId timeZone) { 118 this.databaseType = databaseType == null ? DatabaseType.GENERIC : databaseType; 119 this.timeZone = timeZone == null ? ZoneId.systemDefault() : timeZone; 120 this.timeZoneCalendar = Calendar.getInstance(TimeZone.getTimeZone(this.timeZone)); 121 } 122 123 @Override 124 @Nonnull 125 public <T> Optional<T> map(@Nonnull StatementContext<T> statementContext, 126 @Nonnull ResultSet resultSet, 127 @Nonnull Class<T> resultSetRowType, 128 @Nonnull InstanceProvider instanceProvider) { 129 requireNonNull(statementContext); 130 requireNonNull(resultSet); 131 requireNonNull(resultSetRowType); 132 requireNonNull(instanceProvider); 133 134 try { 135 StandardTypeResult<T> standardTypeResult = mapResultSetToStandardType(resultSet, resultSetRowType); 136 137 if (standardTypeResult.isStandardType()) 138 return standardTypeResult.getValue(); 139 140 if (resultSetRowType.isRecord()) 141 return Optional.ofNullable((T) mapResultSetToRecord((StatementContext<? extends Record>) statementContext, resultSet, instanceProvider)); 142 143 return Optional.ofNullable(mapResultSetToBean(statementContext, resultSet, instanceProvider)); 144 } catch (DatabaseException e) { 145 throw e; 146 } catch (Exception e) { 147 throw new DatabaseException(format("Unable to map JDBC %s to %s", ResultSet.class.getSimpleName(), resultSetRowType), 148 e); 149 } 150 } 151 152 /** 153 * Attempts to map the current {@code resultSet} row to an instance of {@code resultClass} using one of the 154 * "out-of-the-box" types (primitives, common types like {@link UUID}, etc. 155 * <p> 156 * This does not attempt to map to a user-defined JavaBean - see {@link #mapResultSetToBean(StatementContext, ResultSet, InstanceProvider)} for 157 * that functionality. 158 * 159 * @param <T> result instance type token 160 * @param resultSet provides raw row data to pull from 161 * @param resultClass the type of instance to map to 162 * @return the result of the mapping 163 * @throws Exception if an error occurs during mapping 164 */ 165 @SuppressWarnings({"unchecked", "rawtypes"}) 166 @Nonnull 167 protected <T> StandardTypeResult<T> mapResultSetToStandardType(@Nonnull ResultSet resultSet, 168 @Nonnull Class<T> resultClass) throws Exception { 169 requireNonNull(resultSet); 170 requireNonNull(resultClass); 171 172 Object value = null; 173 boolean standardType = true; 174 175 if (resultClass.isAssignableFrom(Byte.class) || resultClass.isAssignableFrom(byte.class)) { 176 value = resultSet.getByte(1); 177 } else if (resultClass.isAssignableFrom(Short.class) || resultClass.isAssignableFrom(short.class)) { 178 value = resultSet.getShort(1); 179 } else if (resultClass.isAssignableFrom(Integer.class) || resultClass.isAssignableFrom(int.class)) { 180 value = resultSet.getInt(1); 181 } else if (resultClass.isAssignableFrom(Long.class) || resultClass.isAssignableFrom(long.class)) { 182 value = resultSet.getLong(1); 183 } else if (resultClass.isAssignableFrom(Float.class) || resultClass.isAssignableFrom(float.class)) { 184 value = resultSet.getFloat(1); 185 } else if (resultClass.isAssignableFrom(Double.class) || resultClass.isAssignableFrom(double.class)) { 186 value = resultSet.getDouble(1); 187 } else if (resultClass.isAssignableFrom(Boolean.class) || resultClass.isAssignableFrom(boolean.class)) { 188 value = resultSet.getBoolean(1); 189 } else if (resultClass.isAssignableFrom(Character.class) || resultClass.isAssignableFrom(char.class)) { 190 String string = resultSet.getString(1); 191 192 if (string != null) 193 if (string.length() == 1) 194 value = string.charAt(0); 195 else 196 throw new DatabaseException(format("Cannot map String value '%s' to %s", resultClass.getSimpleName())); 197 } else if (resultClass.isAssignableFrom(String.class)) { 198 value = resultSet.getString(1); 199 } else if (resultClass.isAssignableFrom(byte[].class)) { 200 value = resultSet.getBytes(1); 201 } else if (resultClass.isAssignableFrom(Enum.class)) { 202 value = Enum.valueOf((Class) resultClass, resultSet.getString(1)); 203 } else if (resultClass.isAssignableFrom(UUID.class)) { 204 String string = resultSet.getString(1); 205 206 if (string != null) 207 value = UUID.fromString(string); 208 } else if (resultClass.isAssignableFrom(BigDecimal.class)) { 209 value = resultSet.getBigDecimal(1); 210 } else if (resultClass.isAssignableFrom(BigInteger.class)) { 211 BigDecimal bigDecimal = resultSet.getBigDecimal(1); 212 213 if (bigDecimal != null) 214 value = bigDecimal.toBigInteger(); 215 } else if (resultClass.isAssignableFrom(Date.class)) { 216 value = resultSet.getTimestamp(1, getTimeZoneCalendar()); 217 } else if (resultClass.isAssignableFrom(Instant.class)) { 218 Timestamp timestamp = resultSet.getTimestamp(1, getTimeZoneCalendar()); 219 220 if (timestamp != null) 221 value = timestamp.toInstant(); 222 } else if (resultClass.isAssignableFrom(LocalDate.class)) { 223 value = resultSet.getObject(1); // DATE 224 } else if (resultClass.isAssignableFrom(LocalTime.class)) { 225 value = resultSet.getObject(1); // TIME 226 } else if (resultClass.isAssignableFrom(LocalDateTime.class)) { 227 value = resultSet.getObject(1); // TIMESTAMP 228 } else if (resultClass.isAssignableFrom(OffsetTime.class)) { 229 value = resultSet.getObject(1); // TIME WITH TIMEZONE 230 } else if (resultClass.isAssignableFrom(OffsetDateTime.class)) { 231 value = resultSet.getObject(1); // TIMESTAMP WITH TIMEZONE 232 } else if (resultClass.isAssignableFrom(java.sql.Date.class)) { 233 value = resultSet.getDate(1, getTimeZoneCalendar()); 234 } else if (resultClass.isAssignableFrom(ZoneId.class)) { 235 String zoneId = resultSet.getString(1); 236 237 if (zoneId != null) 238 value = ZoneId.of(zoneId); 239 } else if (resultClass.isAssignableFrom(TimeZone.class)) { 240 String timeZone = resultSet.getString(1); 241 242 if (timeZone != null) 243 value = TimeZone.getTimeZone(timeZone); 244 } else if (resultClass.isAssignableFrom(Locale.class)) { 245 String locale = resultSet.getString(1); 246 247 if (locale != null) 248 value = Locale.forLanguageTag(locale); 249 } else if (resultClass.isAssignableFrom(Currency.class)) { 250 String currency = resultSet.getString(1); 251 252 if (currency != null) 253 value = Currency.getInstance(currency); 254 } else if (resultClass.isEnum()) { 255 value = extractEnumValue(resultClass, resultSet.getObject(1)); 256 257 // TODO: revisit java.sql.* handling 258 259 // } else if (resultClass.isAssignableFrom(java.sql.Blob.class)) { 260 // value = resultSet.getBlob(1); 261 // } else if (resultClass.isAssignableFrom(java.sql.Clob.class)) { 262 // value = resultSet.getClob(1); 263 // } else if (resultClass.isAssignableFrom(java.sql.Clob.class)) { 264 // value = resultSet.getClob(1); 265 266 } else { 267 standardType = false; 268 } 269 270 if (standardType) { 271 int columnCount = resultSet.getMetaData().getColumnCount(); 272 273 if (columnCount != 1) { 274 List<String> columnLabels = new ArrayList<>(columnCount); 275 276 for (int i = 1; i <= columnCount; ++i) 277 columnLabels.add(resultSet.getMetaData().getColumnLabel(i)); 278 279 throw new DatabaseException(format("Expected 1 column to map to %s but encountered %s instead (%s)", 280 resultClass, columnCount, columnLabels.stream().collect(joining(", ")))); 281 } 282 } 283 284 return new StandardTypeResult(value, standardType); 285 } 286 287 /** 288 * Attempts to map the current {@code resultSet} row to an instance of {@code resultClass}, which must be a 289 * Record. 290 * 291 * @param <T> result instance type token 292 * @param statementContext current SQL context 293 * @param resultSet provides raw row data to pull from 294 * @param instanceProvider an instance-creation factory, used to instantiate resultset row objects as needed. 295 * @return the result of the mapping 296 * @throws Exception if an error occurs during mapping 297 */ 298 @Nonnull 299 protected <T extends Record> T mapResultSetToRecord(@Nonnull StatementContext<T> statementContext, 300 @Nonnull ResultSet resultSet, 301 @Nonnull InstanceProvider instanceProvider) throws Exception { 302 requireNonNull(statementContext); 303 requireNonNull(resultSet); 304 requireNonNull(instanceProvider); 305 306 Class<T> resultSetRowType = statementContext.getResultSetRowType().get(); 307 308 RecordComponent[] recordComponents = resultSetRowType.getRecordComponents(); 309 Map<String, Set<String>> columnLabelAliasesByPropertyName = determineColumnLabelAliasesByPropertyName(resultSetRowType); 310 Map<String, Object> columnLabelsToValues = extractColumnLabelsToValues(resultSet); 311 Object[] args = new Object[recordComponents.length]; 312 313 for (int i = 0; i < recordComponents.length; ++i) { 314 RecordComponent recordComponent = recordComponents[i]; 315 316 String propertyName = recordComponent.getName(); 317 318 // If there are any @DatabaseColumn annotations on this field, respect them 319 Set<String> potentialPropertyNames = columnLabelAliasesByPropertyName.get(propertyName); 320 321 // There were no @DatabaseColumn annotations, use the default naming strategy 322 if (potentialPropertyNames == null || potentialPropertyNames.size() == 0) 323 potentialPropertyNames = databaseColumnNamesForPropertyName(propertyName); 324 325 Class<?> recordComponentType = recordComponent.getType(); 326 327 // Set the value for the Record ctor 328 for (String potentialPropertyName : potentialPropertyNames) { 329 if (columnLabelsToValues.containsKey(potentialPropertyName)) { 330 Object value = convertResultSetValueToPropertyType(columnLabelsToValues.get(potentialPropertyName), recordComponentType).orElse(null); 331 332 if (value != null && !recordComponentType.isAssignableFrom(value.getClass())) { 333 String resultSetTypeDescription = value.getClass().toString(); 334 335 throw new DatabaseException( 336 format( 337 "Property '%s' of %s has a write method of type %s, but the ResultSet type %s does not match. " 338 + "Consider creating your own %s and overriding convertResultSetValueToPropertyType() to detect instances of %s and convert them to %s", 339 recordComponent.getName(), resultSetRowType, recordComponentType, resultSetTypeDescription, 340 DefaultResultSetMapper.class.getSimpleName(), resultSetTypeDescription, recordComponentType)); 341 } 342 343 args[i] = value; 344 } 345 } 346 } 347 348 return instanceProvider.provideRecord(statementContext, resultSetRowType, args); 349 } 350 351 /** 352 * Attempts to map the current {@code resultSet} row to an instance of {@code resultClass}, which should be a 353 * JavaBean. 354 * 355 * @param <T> result instance type token 356 * @param statementContext current SQL context 357 * @param resultSet provides raw row data to pull from 358 * @param instanceProvider an instance-creation factory, used to instantiate resultset row objects as needed. 359 * @return the result of the mapping 360 * @throws Exception if an error occurs during mapping 361 */ 362 @Nonnull 363 protected <T> T mapResultSetToBean(@Nonnull StatementContext<T> statementContext, 364 @Nonnull ResultSet resultSet, 365 @Nonnull InstanceProvider instanceProvider) throws Exception { 366 requireNonNull(statementContext); 367 requireNonNull(resultSet); 368 requireNonNull(instanceProvider); 369 370 Class<T> resultSetRowType = statementContext.getResultSetRowType().get(); 371 372 T object = instanceProvider.provide(statementContext, resultSetRowType); 373 BeanInfo beanInfo = Introspector.getBeanInfo(resultSetRowType); 374 Map<String, Object> columnLabelsToValues = extractColumnLabelsToValues(resultSet); 375 Map<String, Set<String>> columnLabelAliasesByPropertyName = determineColumnLabelAliasesByPropertyName(resultSetRowType); 376 377 for (PropertyDescriptor propertyDescriptor : beanInfo.getPropertyDescriptors()) { 378 Method writeMethod = propertyDescriptor.getWriteMethod(); 379 380 if (writeMethod == null) 381 continue; 382 383 Parameter parameter = writeMethod.getParameters()[0]; 384 385 // Pull in property names, taking into account any aliases defined by @DatabaseColumn 386 Set<String> propertyNames = columnLabelAliasesByPropertyName.get(propertyDescriptor.getName()); 387 388 if (propertyNames == null) 389 propertyNames = new HashSet<>(); 390 else 391 propertyNames = new HashSet<>(propertyNames); 392 393 // If no @DatabaseColumn annotation, then use the field name itself 394 if (propertyNames.size() == 0) 395 propertyNames.add(propertyDescriptor.getName()); 396 397 // Normalize property names to database column names. 398 // For example, a property name of "address1" would get normalized to the set of "address1" and "address_1" by 399 // default 400 propertyNames = 401 propertyNames.stream().map(propertyName -> databaseColumnNamesForPropertyName(propertyName)) 402 .flatMap(columnNames -> columnNames.stream()).collect(toSet()); 403 404 for (String propertyName : propertyNames) { 405 if (columnLabelsToValues.containsKey(propertyName)) { 406 Object value = convertResultSetValueToPropertyType(columnLabelsToValues.get(propertyName), parameter.getType()).orElse(null); 407 Class<?> writeMethodParameterType = writeMethod.getParameterTypes()[0]; 408 409 if (value != null && !writeMethodParameterType.isAssignableFrom(value.getClass())) { 410 String resultSetTypeDescription = value.getClass().toString(); 411 412 throw new DatabaseException( 413 format( 414 "Property '%s' of %s has a write method of type %s, but the ResultSet type %s does not match. " 415 + "Consider creating your own %s and overriding convertResultSetValueToPropertyType() to detect instances of %s and convert them to %s", 416 propertyDescriptor.getName(), resultSetRowType, writeMethodParameterType, resultSetTypeDescription, 417 DefaultResultSetMapper.class.getSimpleName(), resultSetTypeDescription, writeMethodParameterType)); 418 } 419 420 writeMethod.invoke(object, value); 421 } 422 } 423 } 424 425 return object; 426 } 427 428 @Nonnull 429 protected Map<String, Set<String>> determineColumnLabelAliasesByPropertyName(@Nonnull Class<?> resultClass) { 430 requireNonNull(resultClass); 431 432 return columnLabelAliasesByPropertyNameCache.computeIfAbsent( 433 resultClass, 434 (key) -> { 435 Map<String, Set<String>> cachedColumnLabelAliasesByPropertyName = new HashMap<>(); 436 437 for (Field field : resultClass.getDeclaredFields()) { 438 DatabaseColumn databaseColumn = field.getAnnotation(DatabaseColumn.class); 439 440 if (databaseColumn != null) 441 cachedColumnLabelAliasesByPropertyName.put( 442 field.getName(), 443 unmodifiableSet(asList(databaseColumn.value()).stream() 444 .map(columnLabel -> normalizeColumnLabel(columnLabel)).collect(toSet()))); 445 } 446 447 return unmodifiableMap(cachedColumnLabelAliasesByPropertyName); 448 }); 449 } 450 451 @Nonnull 452 protected Map<String, Object> extractColumnLabelsToValues(@Nonnull ResultSet resultSet) throws SQLException { 453 requireNonNull(resultSet); 454 455 ResultSetMetaData resultSetMetaData = resultSet.getMetaData(); 456 int columnCount = resultSetMetaData.getColumnCount(); 457 Set<String> columnLabels = new HashSet<>(columnCount); 458 459 for (int i = 0; i < columnCount; i++) 460 columnLabels.add(resultSetMetaData.getColumnLabel(i + 1)); 461 462 Map<String, Object> columnLabelsToValues = new HashMap<>(columnLabels.size()); 463 464 for (String columnLabel : columnLabels) { 465 Object resultSetValue = resultSet.getObject(columnLabel); 466 467 // If DB gives us time-related values, re-pull using specific RS methods so we can apply a timezone 468 if (resultSetValue != null) { 469 if (resultSetValue instanceof java.sql.Timestamp) { 470 resultSetValue = resultSet.getTimestamp(columnLabel, getTimeZoneCalendar()); 471 } else if (resultSetValue instanceof java.sql.Date) { 472 resultSetValue = resultSet.getDate(columnLabel, getTimeZoneCalendar()); 473 } else if (resultSetValue instanceof java.sql.Time) { 474 resultSetValue = resultSet.getTime(columnLabel, getTimeZoneCalendar()); 475 } 476 } 477 478 columnLabelsToValues.put(normalizeColumnLabel(columnLabel), resultSetValue); 479 } 480 481 return columnLabelsToValues; 482 } 483 484 /** 485 * Massages a {@link ResultSet#getObject(String)} value to match the given {@code propertyType}. 486 * <p> 487 * For example, the JDBC driver might give us {@link java.sql.Timestamp} but our corresponding JavaBean field is of 488 * type {@link java.util.Date}, so we need to manually convert that ourselves. 489 * 490 * @param resultSetValue the value returned by {@link ResultSet#getObject(String)} 491 * @param propertyType the JavaBean property type we'd like to map {@code resultSetValue} to 492 * @return a representation of {@code resultSetValue} that is of type {@code propertyType} 493 */ 494 @Nonnull 495 protected Optional<Object> convertResultSetValueToPropertyType(@Nullable Object resultSetValue, 496 @Nonnull Class<?> propertyType) { 497 requireNonNull(propertyType); 498 499 if (resultSetValue == null) 500 return Optional.empty(); 501 502 if (resultSetValue instanceof BigDecimal) { 503 BigDecimal bigDecimal = (BigDecimal) resultSetValue; 504 505 if (BigDecimal.class.isAssignableFrom(propertyType)) 506 return Optional.ofNullable(bigDecimal); 507 if (BigInteger.class.isAssignableFrom(propertyType)) 508 return Optional.of(bigDecimal.toBigInteger()); 509 } 510 511 if (resultSetValue instanceof BigInteger) { 512 BigInteger bigInteger = (BigInteger) resultSetValue; 513 514 if (BigDecimal.class.isAssignableFrom(propertyType)) 515 return Optional.of(new BigDecimal(bigInteger)); 516 if (BigInteger.class.isAssignableFrom(propertyType)) 517 return Optional.ofNullable(bigInteger); 518 } 519 520 if (resultSetValue instanceof Number) { 521 Number number = (Number) resultSetValue; 522 523 if (Byte.class.isAssignableFrom(propertyType)) 524 return Optional.of(number.byteValue()); 525 if (Short.class.isAssignableFrom(propertyType)) 526 return Optional.of(number.shortValue()); 527 if (Integer.class.isAssignableFrom(propertyType)) 528 return Optional.of(number.intValue()); 529 if (Long.class.isAssignableFrom(propertyType)) 530 return Optional.of(number.longValue()); 531 if (Float.class.isAssignableFrom(propertyType)) 532 return Optional.of(number.floatValue()); 533 if (Double.class.isAssignableFrom(propertyType)) 534 return Optional.of(number.doubleValue()); 535 if (BigDecimal.class.isAssignableFrom(propertyType)) 536 return Optional.of(BigDecimal.valueOf((number.doubleValue()))); 537 if (BigInteger.class.isAssignableFrom(propertyType)) 538 return Optional.of(BigDecimal.valueOf(number.doubleValue()).toBigInteger()); 539 } else if (resultSetValue instanceof java.sql.Timestamp) { 540 java.sql.Timestamp date = (java.sql.Timestamp) resultSetValue; 541 542 if (Date.class.isAssignableFrom(propertyType)) 543 return Optional.ofNullable(date); 544 if (Instant.class.isAssignableFrom(propertyType)) 545 return Optional.of(date.toInstant()); 546 if (LocalDate.class.isAssignableFrom(propertyType)) 547 return Optional.of(date.toInstant().atZone(getTimeZone()).toLocalDate()); 548 if (LocalDateTime.class.isAssignableFrom(propertyType)) 549 return Optional.of(date.toLocalDateTime()); 550 } else if (resultSetValue instanceof java.sql.Date) { 551 java.sql.Date date = (java.sql.Date) resultSetValue; 552 553 if (Date.class.isAssignableFrom(propertyType)) 554 return Optional.ofNullable(date); 555 if (Instant.class.isAssignableFrom(propertyType)) 556 return Optional.of(date.toInstant()); 557 if (LocalDate.class.isAssignableFrom(propertyType)) 558 return Optional.of(date.toLocalDate()); 559 if (LocalDateTime.class.isAssignableFrom(propertyType)) 560 return Optional.of(LocalDateTime.ofInstant(date.toInstant(), getTimeZone())); 561 } else if (resultSetValue instanceof java.sql.Time) { 562 java.sql.Time time = (java.sql.Time) resultSetValue; 563 564 if (LocalTime.class.isAssignableFrom(propertyType)) 565 return Optional.ofNullable(time.toLocalTime()); 566 } else if (propertyType.isAssignableFrom(ZoneId.class)) { 567 return Optional.ofNullable(ZoneId.of(resultSetValue.toString())); 568 } else if (propertyType.isAssignableFrom(TimeZone.class)) { 569 return Optional.ofNullable(TimeZone.getTimeZone(resultSetValue.toString())); 570 } else if (propertyType.isAssignableFrom(Locale.class)) { 571 return Optional.ofNullable(Locale.forLanguageTag(resultSetValue.toString())); 572 } else if (propertyType.isAssignableFrom(Currency.class)) { 573 return Optional.ofNullable(Currency.getInstance(resultSetValue.toString())); 574 } else if (propertyType.isEnum()) { 575 return Optional.ofNullable(extractEnumValue(propertyType, resultSetValue)); 576 } else if ("org.postgresql.util.PGobject".equals(resultSetValue.getClass().getName())) { 577 org.postgresql.util.PGobject pgObject = (org.postgresql.util.PGobject) resultSetValue; 578 return Optional.ofNullable(pgObject.getValue()); 579 } 580 581 return Optional.ofNullable(resultSetValue); 582 } 583 584 /** 585 * Attempts to convert {@code object} to a corresponding value for enum type {@code enumClass}. 586 * <p> 587 * Normally {@code object} is a {@code String}, but other types may be used - the {@code toString()} method of 588 * {@code object} will be invoked to determine the final value for conversion. 589 * 590 * @param enumClass the enum to which we'd like to convert {@code object} 591 * @param object the object to convert to an enum value 592 * @return the enum value of {@code object} for {@code enumClass} 593 * @throws DatabaseException if {@code object} does not correspond to a valid enum value 594 */ 595 @SuppressWarnings({"unchecked", "rawtypes"}) 596 @Nonnull 597 protected Enum<?> extractEnumValue(@Nonnull Class<?> enumClass, 598 @Nonnull Object object) { 599 requireNonNull(enumClass); 600 requireNonNull(object); 601 602 if (!enumClass.isEnum()) 603 throw new IllegalArgumentException(format("%s is not an enum type", enumClass)); 604 605 String objectAsString = object.toString(); 606 607 try { 608 return Enum.valueOf((Class<? extends Enum>) enumClass, objectAsString); 609 } catch (IllegalArgumentException | NullPointerException e) { 610 throw new DatabaseException(format("The value '%s' is not present in enum %s", objectAsString, enumClass), e); 611 } 612 } 613 614 /** 615 * Massages a {@link ResultSet} column label so it's easier to match against a JavaBean property name. 616 * <p> 617 * This implementation lowercases the label using the locale provided by {@link #getNormalizationLocale()}. 618 * 619 * @param columnLabel the {@link ResultSet} column label to massage 620 * @return the massaged label 621 */ 622 @Nonnull 623 protected String normalizeColumnLabel(@Nonnull String columnLabel) { 624 requireNonNull(columnLabel); 625 return columnLabel.toLowerCase(getNormalizationLocale()); 626 } 627 628 /** 629 * Massages a JavaBean property name to match standard database column name (camelCase -> camel_case). 630 * <p> 631 * Uses {@link #getNormalizationLocale()} to perform case-changing. 632 * <p> 633 * There may be multiple database column name mappings, for example property {@code address1} might map to both 634 * {@code address1} and {@code address_1} column names. 635 * 636 * @param propertyName the JavaBean property name to massage 637 * @return the column names that match the JavaBean property name 638 */ 639 @Nonnull 640 protected Set<String> databaseColumnNamesForPropertyName(@Nonnull String propertyName) { 641 requireNonNull(propertyName); 642 643 Set<String> normalizedPropertyNames = new HashSet<>(2); 644 645 // Converts camelCase to camel_case 646 String camelCaseRegex = "([a-z])([A-Z]+)"; 647 String replacement = "$1_$2"; 648 649 String normalizedPropertyName = 650 propertyName.replaceAll(camelCaseRegex, replacement).toLowerCase(getNormalizationLocale()); 651 normalizedPropertyNames.add(normalizedPropertyName); 652 653 // Converts address1 to address_1 654 String letterFollowedByNumberRegex = "(\\D)(\\d)"; 655 String normalizedNumberPropertyName = normalizedPropertyName.replaceAll(letterFollowedByNumberRegex, replacement); 656 normalizedPropertyNames.add(normalizedNumberPropertyName); 657 658 return normalizedPropertyNames; 659 } 660 661 /** 662 * The locale to use when massaging JDBC column names for matching against JavaBean property names. 663 * <p> 664 * Used by {@link #normalizeColumnLabel(String)}. 665 * 666 * @return the locale to use for massaging, hardcoded to {@link Locale#ENGLISH} by default 667 */ 668 @Nonnull 669 protected Locale getNormalizationLocale() { 670 return ENGLISH; 671 } 672 673 /** 674 * What kind of database are we working with? 675 * 676 * @return the kind of database we're working with 677 */ 678 @Nonnull 679 protected DatabaseType getDatabaseType() { 680 return this.databaseType; 681 } 682 683 @Nonnull 684 protected ZoneId getTimeZone() { 685 return this.timeZone; 686 } 687 688 @Nonnull 689 protected Calendar getTimeZoneCalendar() { 690 return this.timeZoneCalendar; 691 } 692 693 /** 694 * The result of attempting to map a {@link ResultSet} to a "standard" type like primitive or {@link UUID}. 695 * 696 * @author <a href="https://www.revetkn.com">Mark Allen</a> 697 * @since 1.0.0 698 */ 699 @ThreadSafe 700 protected static class StandardTypeResult<T> { 701 @Nullable 702 private final T value; 703 @Nonnull 704 private final Boolean standardType; 705 706 /** 707 * Creates a {@code StandardTypeResult} with the given {@code value} and {@code standardType} flag. 708 * 709 * @param value the mapping result, may be {@code null} 710 * @param standardType {@code true} if the mapped type was a standard type, {@code false} otherwise 711 */ 712 public StandardTypeResult(@Nullable T value, 713 @Nonnull Boolean standardType) { 714 requireNonNull(standardType); 715 716 this.value = value; 717 this.standardType = standardType; 718 } 719 720 /** 721 * Gets the result of the mapping. 722 * 723 * @return the mapping result value, may be {@code null} 724 */ 725 @Nonnull 726 public Optional<T> getValue() { 727 return Optional.ofNullable(this.value); 728 } 729 730 /** 731 * Was the mapped type a standard type? 732 * 733 * @return {@code true} if this was a standard type, {@code false} otherwise 734 */ 735 @Nonnull 736 public Boolean isStandardType() { 737 return this.standardType; 738 } 739 } 740}