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

Class Method, % Line, %
Converter 100% (11/11) 98.3% (232/236)
Converter$1 100% (1/1) 100% (1/1)
Total 100% (12/12) 98.3% (233/237)


1 package net.sf.persism; 2  3 import java.math.BigDecimal; 4 import java.nio.ByteBuffer; 5 import java.sql.Blob; 6 import java.sql.Time; 7 import java.sql.Timestamp; 8 import java.text.DateFormat; 9 import java.text.ParseException; 10 import java.text.SimpleDateFormat; 11 import java.time.LocalDate; 12 import java.time.LocalDateTime; 13 import java.time.LocalTime; 14 import java.time.ZoneId; 15 import java.util.Date; 16 import java.util.UUID; 17  18 final class Converter { 19  20  private static final Log log = Log.getLogger(Converter.class); 21  22  // https://stackoverflow.com/questions/2409657/call-to-method-of-static-java-text-dateformat-not-advisable 23  // https://www.javacodegeeks.com/2010/07/java-best-practices-dateformat-in.html 24  private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT1 = 25  ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss")); 26  27  private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT2 = 28  ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd")); 29  30  private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT3 = 31  ThreadLocal.withInitial(() -> new SimpleDateFormat("hh:mm:ss")); 32  33  // Make a sensible conversion of the value type from the DB and the property type defined 34  // on the Data class - or the value type from the property to the statement parameter. 35  Object convert(Object value, Class<?> targetType, String columnName) { 36  assert value != null; 37  38  JavaType valueType = JavaType.getType(value.getClass()); 39  40  if (valueType == null) { 41  log.warn(Message.NoConversionForUnknownType.message(value.getClass())); 42  return value; 43  } 44  45  Object returnValue = value; 46  47  // try to convert or cast (no cast) the value to the proper type. 48  switch (valueType) { 49  50  case booleanType: 51  case BooleanType: 52  break; 53  54  case byteType: 55  case ByteType: 56  log.warnNoDuplicates(Message.TinyIntMSSQL.message(columnName)); 57  break; 58  59  case shortType: 60  case ShortType: 61  break; 62  63  case integerType: 64  case IntegerType: 65  66  if (targetType == Integer.class || targetType == int.class) { 67  break; 68  } 69  70  // int to bool 71  if (targetType == Boolean.class || targetType == boolean.class) { 72  returnValue = Integer.parseInt("" + value) != 0; 73  break; 74  } 75  76  if (targetType == Short.class || targetType == short.class) { 77  log.warnNoDuplicates(Message.PossibleOverflow.message(columnName, "SHORT", "INT")); 78  returnValue = Short.parseShort("" + value); 79  break; 80  } 81  82  if (targetType == Byte.class || targetType == byte.class) { 83  log.warnNoDuplicates(Message.PossibleOverflow.message(columnName, "BYTE", "INT")); 84  returnValue = Byte.parseByte("" + value); 85  break; 86  } 87  88  if (targetType == Time.class) { 89  // SQLite when a Time is defined VIA a convert from LocalTime via Time.valueOf (see getContactForTest) 90  returnValue = new Time(((Integer) value).longValue()); 91  break; 92  } 93  94  if (targetType == LocalTime.class) { 95  // SQLite for Time SQLite sees Long, for LocalTime it sees Integer 96  returnValue = new Time((Integer) value).toLocalTime(); 97  break; 98  } 99  100  break; 101  102  case longType: 103  case LongType: 104  105  if (targetType == Long.class || targetType == long.class) { 106  break; 107  } 108  109  long lval = Long.parseLong("" + value); 110  if (targetType == java.sql.Date.class) { 111  returnValue = new java.sql.Date(lval); 112  break; 113  } 114  115  if (targetType == Date.class) { 116  returnValue = new java.util.Date(lval); 117  break; 118  } 119  120  if (targetType == Timestamp.class) { 121  returnValue = new Timestamp(lval); 122  break; 123  } 124  125  if (targetType == Integer.class || targetType == int.class) { 126  log.warnNoDuplicates(Message.PossibleOverflow.message(columnName, "INT", "LONG")); 127  returnValue = Integer.parseInt("" + lval); 128  break; 129  } 130  131  if (targetType == LocalDateTime.class) { 132  returnValue = new Timestamp(lval).toLocalDateTime(); 133  break; 134  } 135  136  if (targetType == LocalDate.class) { 137  // SQLite reads long as date..... 138  returnValue = new Timestamp(lval).toLocalDateTime().toLocalDate(); 139  break; 140  } 141  142  if (targetType == Time.class) { 143  // SQLite.... Again..... 144  returnValue = new Time((Long) value); 145  break; 146  } 147  break; 148  149  case floatType: 150  case FloatType: 151  if (targetType == Float.class || targetType == float.class) { 152  break; 153  } 154  155  Float flt = (Float) value; 156  if (targetType == Integer.class || targetType == int.class) { 157  log.warnNoDuplicates(Message.PossibleOverflow.message(columnName, "INT", "DOUBLE")); 158  returnValue = flt.intValue(); 159  break; 160  } 161  162  if (targetType == Long.class || targetType == long.class) { 163  log.warnNoDuplicates(Message.PossibleOverflow.message(columnName, "LONG", "DOUBLE")); 164  returnValue = flt.longValue(); 165  break; 166  } 167  break; 168  169  case doubleType: 170  case DoubleType: 171  if (targetType == Double.class || targetType == double.class) { 172  break; 173  } 174  175  Double dbl = (Double) value; 176  // float or doubles to BigDecimal 177  if (targetType == BigDecimal.class) { 178  returnValue = new BigDecimal("" + value); 179  break; 180  } 181  182  if (targetType == Float.class || targetType == float.class) { 183  log.warnNoDuplicates(Message.PossibleOverflow.message(columnName, "FLOAT", "DOUBLE")); 184  returnValue = dbl.floatValue(); 185  break; 186  } 187  188  if (targetType == Integer.class || targetType == int.class) { 189  log.warnNoDuplicates(Message.PossibleOverflow.message(columnName, "INT", "DOUBLE")); 190  returnValue = dbl.intValue(); 191  break; 192  } 193  194  if (targetType == Long.class || targetType == long.class) { 195  log.warnNoDuplicates(Message.PossibleOverflow.message(columnName, "LONG", "DOUBLE")); 196  returnValue = dbl.longValue(); 197  break; 198  } 199  break; 200  201  case BigDecimalType: 202  if (targetType == BigDecimal.class) { 203  break; 204  } 205  206  if (targetType == Float.class || targetType == float.class) { 207  returnValue = ((Number) value).floatValue(); 208  log.warnNoDuplicates(Message.PossibleOverflow.message(columnName, "FLOAT", "BigDecimal")); 209  break; 210  } 211  212  if (targetType == Double.class || targetType == double.class) { 213  returnValue = ((Number) value).doubleValue(); 214  log.warnNoDuplicates(Message.PossibleOverflow.message(columnName, "DOUBLE", "BigDecimal")); 215  break; 216  } 217  218  if (targetType == Long.class || targetType == long.class) { 219  returnValue = ((Number) value).longValue(); 220  log.warnNoDuplicates(Message.PossibleOverflow.message(columnName, "LONG", "BigDecimal")); 221  break; 222  } 223  224  if (targetType == String.class) { 225  returnValue = (value).toString(); 226  break; 227  } 228  229  if (targetType == Integer.class || targetType == int.class) { 230  returnValue = ((Number) value).intValue(); 231  log.warnNoDuplicates(Message.PossibleOverflow.message(columnName, "INT", "BigDecimal")); 232  break; 233  } 234  235  if (targetType == Short.class || targetType == short.class) { 236  returnValue = ((Number) value).shortValue(); 237  log.warnNoDuplicates(Message.PossibleOverflow.message(columnName, "SHORT", "BigDecimal")); 238  break; 239  } 240  241  if (targetType == Boolean.class || targetType == boolean.class) { 242  // BigDecimal to Boolean. Oracle (sigh) - Additional for a Char to Boolean as then (see TestOracle for links) 243  returnValue = ((Number) value).intValue() == 1; 244  log.warnNoDuplicates(Message.PossibleOverflow.message(columnName, "BOOLEAN", "BigDecimal")); 245  break; 246  } 247  248  break; 249  250  case StringType: 251  if (targetType == String.class) { 252  break; 253  } 254  255  Date dval; 256  DateFormat df; 257  if (("" + value).length() > "yyyy-MM-dd".length()) { 258  df = DATE_FORMAT1.get(); 259  } else { 260  df = DATE_FORMAT2.get(); 261  } 262  263  // Read a string but we want a date 264  if (targetType == Date.class) { 265  // This condition occurs in SQLite when you have a datetime with default annotated 266  // the format returned is 2012-06-02 19:59:49 267  // Used for SQLite returning dates as Strings under some conditions 268  // SQL or others may return STRING yyyy-MM-dd for older legacy 'date' type. 269  // https://docs.microsoft.com/en-us/sql/t-sql/data-types/date-transact-sql?view=sql-server-ver15 270  returnValue = tryParseDate(value, targetType, columnName, df); 271  break; 272  } 273  274  if (targetType == Timestamp.class) { 275  returnValue = tryParseTimestamp(value, targetType, columnName); 276  break; 277  } 278  279  if (targetType == LocalDate.class) { 280  // SQLite 281  dval = tryParseDate(value, targetType, columnName, df); 282  returnValue = dval.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); 283  break; 284  } 285  286  if (targetType == LocalDateTime.class) { 287  // SQLite 288  dval = tryParseDate(value, targetType, columnName, df); 289  returnValue = dval.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(); 290  break; 291  } 292  293  if (targetType.isEnum()) { 294  // If this is an enum do a case-insensitive comparison 295  Object[] enumConstants = targetType.getEnumConstants(); 296  for (Object element : enumConstants) { 297  if (("" + value).equalsIgnoreCase(element.toString())) { 298  returnValue = element; 299  break; 300  } 301  } 302  break; 303  } 304  305  if (targetType == UUID.class) { 306  returnValue = UUID.fromString("" + value); 307  break; 308  } 309  310  if (targetType == Boolean.class || targetType == boolean.class) { 311  // String to Boolean - T or 1 - otherwise false (or null) 312  String bval = ("" + value).toUpperCase(); 313  returnValue = bval.startsWith("T") || bval.startsWith("1"); 314  break; 315  } 316  317  if (targetType == BigDecimal.class) { 318  try { 319  returnValue = new BigDecimal("" + value); 320  } catch (NumberFormatException e) { 321  throw new PersismException(Message.NumberFormatException.message(columnName, targetType, value.getClass(), value), e); 322  } 323  break; 324  } 325  break; 326  327  case characterType: 328  case CharacterType: 329  break; 330  331  case LocalDateType: 332  returnValue = java.sql.Date.valueOf((LocalDate) value); 333  break; 334  335  case LocalDateTimeType: 336  returnValue = Timestamp.valueOf((LocalDateTime) value); 337  break; 338  339  case LocalTimeType: 340  returnValue = Time.valueOf((LocalTime) value); 341  break; 342  343  case UtilDateType: 344  if (targetType == java.sql.Date.class) { 345  returnValue = new java.sql.Date(((Date) value).getTime()); 346  break; 347  } 348  349  if (targetType == Timestamp.class) { 350  returnValue = new Timestamp(((Date) value).getTime()); 351  break; 352  } 353  break; 354  355  case SQLDateType: 356  if (targetType == java.sql.Date.class) { 357  break; 358  } 359  360  if (targetType == Date.class) { 361  returnValue = new java.util.Date(((Date) value).getTime()); 362  break; 363  } 364  365  if (targetType == Timestamp.class) { 366  returnValue = new Timestamp(((Date) value).getTime()); 367  break; 368  } 369  370  if (targetType == LocalDate.class) { 371  Date dt = new Date(((Date) value).getTime()); 372  returnValue = dt.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); 373  break; 374  } 375  376  if (targetType == LocalDateTime.class) { 377  Date dt = new Date(((Date) value).getTime()); 378  returnValue = dt.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(); 379  break; 380  } 381  break; 382  383  case TimestampType: 384  if (targetType == Timestamp.class) { 385  break; 386  } 387  388  if (targetType == Date.class) { 389  returnValue = new java.util.Date(((Date) value).getTime()); 390  break; 391  } 392  393  if (targetType == java.sql.Date.class) { 394  returnValue = new java.sql.Date(((Date) value).getTime()); 395  break; 396  } 397  398  if (targetType == LocalDate.class) { 399  Date dt = new Date(((Date) value).getTime()); 400  returnValue = dt.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); 401  break; 402  } 403  404  if (targetType == LocalDateTime.class) { 405  Date dt = new Date(((Date) value).getTime()); 406  returnValue = dt.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(); 407  break; 408  } 409  410  if (targetType == Time.class) { 411  // Oracle doesn't seem to have Time so we use Timestamp 412  returnValue = new Time(((Date) value).getTime()); 413  break; 414  } 415  416  if (targetType == LocalTime.class) { 417  // Oracle.... Sigh 418  Date dt = (Date) value; 419  returnValue = dt.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime().toLocalTime(); 420  break; 421  } 422  break; 423  424  case TimeType: 425  if (targetType == LocalTime.class) { 426  returnValue = LocalTime.parse("" + value); 427  } 428  break; 429  430  case InstantType: 431  case OffsetDateTimeType: 432  case ZonedDateTimeType: 433  log.warn(Message.ConverterValueTypeNotYetSupported.message(valueType.getJavaType()), new Throwable()); 434  break; 435  436  case byteArrayType: 437  case ByteArrayType: 438  if (targetType == UUID.class) { 439  returnValue = asUuid((byte[]) value); 440  } 441  break; 442  443  case ClobType: 444  case BlobType: 445  log.warn(Message.ConverterDoNotUseClobOrBlobAsAPropertyType.message(), new Throwable()); 446  break; 447  448  case EnumType: 449  // No need to convert it here. 450  // If it's being used for the property setter then it's OK 451  // If it's being used by setParameters it's converted to String 452  // The String case above converts from the String to the Enum 453  log.debug("EnumType"); 454  break; 455  456  case UUIDType: 457  if (targetType == Blob.class || targetType == byte[].class || targetType == Byte[].class) { 458  returnValue = asBytes((UUID) value); 459  } 460  break; 461  462  case ObjectType: 463  break; 464  } 465  return returnValue; 466  } 467  468  /* 469  * Used by convert for convenience - common possible parsing 470  */ 471  static Date tryParseDate(Object value, Class<?> targetType, String columnName, DateFormat df) throws PersismException { 472  try { 473  return df.parse("" + value); 474  } catch (ParseException e) { 475  throw new PersismException(Message.DateFormatException.message(e.getMessage(), columnName, targetType, value.getClass(), value), e); 476  } 477  } 478  479  static Timestamp tryParseTimestamp(Object value, Class<?> targetType, String columnName) throws PersismException { 480  try { 481  return Timestamp.valueOf("" + value); 482  } catch (IllegalArgumentException e) { 483  throw new PersismException(Message.DateFormatException.message(e.getMessage(), columnName, targetType, value.getClass(), value), e); 484  } 485  } 486  487  // todo document UUID usage for supported and non-supported DBs. Point out this URL to get converter code - otherwise we could expose these 2 static methods somewhere (which we dont want to do) 488  // https://stackoverflow.com/questions/17893609/convert-uuid-to-byte-that-works-when-using-uuid-nameuuidfrombytesb 489  // THANKS! 490  static UUID asUuid(byte[] bytes) { 491  ByteBuffer bb = ByteBuffer.wrap(bytes); 492  long firstLong = bb.getLong(); 493  long secondLong = bb.getLong(); 494  return new UUID(firstLong, secondLong); 495  } 496  497  static byte[] asBytes(UUID uuid) { 498  ByteBuffer bb = ByteBuffer.wrap(new byte[16]); 499  bb.putLong(uuid.getMostSignificantBits()); 500  bb.putLong(uuid.getLeastSignificantBits()); 501  return bb.array(); 502  } 503  504  static Object asBytesFromUUID(UUID uuid) { 505  return asBytes(uuid); 506  } 507  508  509 }