docs: @SuppressWarnings({"PMD.ExcessiveMethodLength", "PMD.NPathComplexity", "PMD.NcssMethodCount", "PMD.CyclomaticComplexity", "PMD.StdCyclomaticComplexity", "PMD.ModifiedCyclomaticComplexity"})
      // CHECKSTYLE:ON
      public static ConnectionProperties build(final Map<String, String> properties) {
  
          Validate.notNull(properties, "The validated object 'properties' is null");
  
          final ConnectionProperties connProps = new ConnectionProperties();
          String tmp = properties.get(KEY_DRIVER);
          if (StringUtils.isNotEmpty(tmp)) { // driver cannot be blank, defaulting to null to catch it
              connProps.setDriver(tmp);
              logValue(KEY_DRIVER, tmp);
          } else {
              connProps.setDriver(DEFAULT_DRIVER);
              logDefault(KEY_DRIVER, DEFAULT_DRIVER);
          }
  
          tmp = properties.get(KEY_URL);
          if (StringUtils.isNotEmpty(tmp)) { // url cannot be blank, defaulting to null to catch it
              connProps.setUrl(tmp);
              logValue(KEY_URL, tmp);
          } else {
              connProps.setUrl(DEFAULT_URL);
              logDefault(KEY_URL, DEFAULT_URL);
          }
  
          tmp = properties.get(KEY_USERNAME);
          if (tmp == null) { // username may be a blank string, but not null
              connProps.setUsername(DEFAULT_USERNAME);
              logDefault(KEY_USERNAME, DEFAULT_USERNAME);
          } else {
              connProps.setUsername(tmp);
              logValue(KEY_USERNAME, tmp);
          }
  
          tmp = properties.get(KEY_PASSWORD);
          if (tmp == null) { // password may be a blank string, but not null
              connProps.setPassword(DEFAULT_PASSWORD);
              logDefault(KEY_PASSWORD, DEFAULT_PASSWORD);
          } else {
              connProps.setPassword(tmp);
              logValue(KEY_PASSWORD, tmp);
          }
  
          tmp = properties.get(KEY_MAX_TOTAL);
          if (StringUtils.isNotEmpty(tmp)) {
              if (StringUtils.isNumeric(tmp)) {
                  connProps.setMaxTotal(Integer.decode(tmp));
                  logValue(KEY_MAX_TOTAL, tmp);
              } else {
                  connProps.setMaxTotal(DEFAULT_MAX_TOTAL);
                  logDefault(KEY_MAX_TOTAL, tmp, "not numeric", String.valueOf(DEFAULT_MAX_TOTAL));
              }
          } else {
              connProps.setMaxTotal(DEFAULT_MAX_TOTAL);
              logDefault(KEY_MAX_TOTAL, String.valueOf(DEFAULT_MAX_TOTAL));
          }
  
          tmp = properties.get(KEY_MAX_IDLE);
          if (StringUtils.isNotEmpty(tmp)) {
              if (StringUtils.isNumeric(tmp)) {
                  connProps.setMaxIdle(Integer.decode(tmp));
                  logValue(KEY_MAX_IDLE, tmp);
              } else {
                  connProps.setMaxIdle(DEFAULT_MAX_IDLE);
                  logDefault(KEY_MAX_IDLE, tmp, "not numeric", String.valueOf(DEFAULT_MAX_IDLE));
              }
          } else {
              connProps.setMaxIdle(DEFAULT_MAX_IDLE);
              logDefault(KEY_MAX_IDLE, String.valueOf(DEFAULT_MAX_IDLE));
          }
  
          tmp = properties.get(KEY_MIN_IDLE);
          if (StringUtils.isNotEmpty(tmp)) {
              if (StringUtils.isNumeric(tmp)) {
                  connProps.setMinIdle(Integer.decode(tmp));
                  logValue(KEY_MIN_IDLE, tmp);
              } else {
                  connProps.setMinIdle(DEFAULT_MIN_IDLE);
                  logDefault(KEY_MIN_IDLE, tmp, "not numeric", String.valueOf(DEFAULT_MIN_IDLE));
              }
          } else {
              connProps.setMinIdle(DEFAULT_MIN_IDLE);
              logDefault(KEY_MIN_IDLE, String.valueOf(DEFAULT_MIN_IDLE));
          }
  
          tmp = properties.get(KEY_MAX_WAIT_MILLIS);
          if (StringUtils.isNotEmpty(tmp)) {
              if (StringUtils.isNumeric(tmp)) {
                  connProps.setMaxWaitMillis(Long.decode(tmp));
                  logValue(KEY_MAX_WAIT_MILLIS, tmp);
              } else {
                  connProps.setMaxWaitMillis(DEFAULT_MAX_WAIT_MILLIS);
                  logDefault(KEY_MAX_WAIT_MILLIS, tmp, "not numeric", String.valueOf(DEFAULT_MAX_WAIT_MILLIS));
              }
          } else {
              connProps.setMaxWaitMillis(DEFAULT_MAX_WAIT_MILLIS);
              logDefault(KEY_MAX_WAIT_MILLIS, String.valueOf(DEFAULT_MAX_WAIT_MILLIS));
          }
  
          tmp = properties.get(KEY_TEST_ON_CREATE);
          if (StringUtils.isNotEmpty(tmp)) {
              connProps.setTestOnCreate(Boolean.parseBoolean(tmp));
              logValue(KEY_TEST_ON_CREATE, tmp);
          } else {
              connProps.setTestOnCreate(DEFAULT_TEST_ON_CREATE);
              logDefault(KEY_TEST_ON_CREATE, String.valueOf(DEFAULT_TEST_ON_CREATE));
          }
  
          tmp = properties.get(KEY_TEST_ON_BORROW);
          if (StringUtils.isNotEmpty(tmp)) {
              connProps.setTestOnBorrow(Boolean.parseBoolean(tmp));
              logValue(KEY_TEST_ON_BORROW, tmp);
          } else {
              connProps.setTestOnBorrow(DEFAULT_TEST_ON_BORROW);
              logDefault(KEY_TEST_ON_BORROW, String.valueOf(DEFAULT_TEST_ON_BORROW));
          }
  
          tmp = properties.get(KEY_TEST_ON_RETURN);
          if (StringUtils.isNotEmpty(tmp)) {
              connProps.setTestOnReturn(Boolean.parseBoolean(tmp));
              logValue(KEY_TEST_ON_RETURN, tmp);
          } else {
              connProps.setTestOnReturn(DEFAULT_TEST_ON_RETURN);
              logDefault(KEY_TEST_ON_RETURN, String.valueOf(DEFAULT_TEST_ON_RETURN));
          }
  
          tmp = properties.get(KEY_TEST_WHILE_IDLE);
          if (StringUtils.isNotEmpty(tmp)) {
              connProps.setTestWhileIdle(Boolean.parseBoolean(tmp));
              logValue(KEY_TEST_WHILE_IDLE, tmp);
          } else {
              connProps.setTestWhileIdle(DEFAULT_TEST_WHILE_IDLE);
              logDefault(KEY_TEST_WHILE_IDLE, String.valueOf(DEFAULT_TEST_WHILE_IDLE));
          }
  
          tmp = properties.get(KEY_TIME_BETWEEN_EVICTION_RUNS_MILLIS);
          if (StringUtils.isNotEmpty(tmp)) {
              if (StringUtils.isNumeric(tmp)) {
                  connProps.setTimeBetweenEvictionRunsMillis(Long.decode(tmp));
                  logValue(KEY_TIME_BETWEEN_EVICTION_RUNS_MILLIS, tmp);
              } else {
                  connProps.setTimeBetweenEvictionRunsMillis(DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS);
                  logDefault(KEY_TIME_BETWEEN_EVICTION_RUNS_MILLIS, tmp, "not numeric",
                          String.valueOf(DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS));
              }
          } else {
              connProps.setTimeBetweenEvictionRunsMillis(DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS);
              logDefault(KEY_TIME_BETWEEN_EVICTION_RUNS_MILLIS,
                      String.valueOf(DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS));
          }
  
          tmp = properties.get(KEY_NUM_TESTS_PER_EVICTION_RUN);
          if (StringUtils.isNotEmpty(tmp)) {
              if (StringUtils.isNumeric(tmp)) {
                  connProps.setNumTestsPerEvictionRun(Integer.decode(tmp));
                  logValue(KEY_NUM_TESTS_PER_EVICTION_RUN, tmp);
              } else {
                  connProps.setNumTestsPerEvictionRun(DEFAULT_NUM_TESTS_PER_EVICITON_RUN);
                  logDefault(KEY_NUM_TESTS_PER_EVICTION_RUN, tmp, "not numeric",
                          String.valueOf(DEFAULT_NUM_TESTS_PER_EVICITON_RUN));
              }
          } else {
              connProps.setNumTestsPerEvictionRun(DEFAULT_NUM_TESTS_PER_EVICITON_RUN);
              logDefault(KEY_NUM_TESTS_PER_EVICTION_RUN, String.valueOf(DEFAULT_NUM_TESTS_PER_EVICITON_RUN));
          }
  
          tmp = properties.get(KEY_MIN_EVICTABLE_IDLE_TIME_MILLIS);
          if (StringUtils.isNotEmpty(tmp)) {
              if (StringUtils.isNumeric(tmp)) {
                  connProps.setMinEvictableIdleTimeMillis(Long.decode(tmp));
                  logValue(KEY_MIN_EVICTABLE_IDLE_TIME_MILLIS, tmp);
              } else {
                  connProps.setMinEvictableIdleTimeMillis(DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS);
                  logDefault(KEY_MIN_EVICTABLE_IDLE_TIME_MILLIS, tmp, "not numeric",
                          String.valueOf(DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS));
              }
          } else {
              connProps.setMinEvictableIdleTimeMillis(DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS);
              logDefault(KEY_MIN_EVICTABLE_IDLE_TIME_MILLIS, String.valueOf(DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS));
          }
  
          tmp = properties.get(KEY_SOFT_MIN_EVICTABLE_IDLE_TIME_MILLIS);
          if (StringUtils.isNotEmpty(tmp)) {
              if (StringUtils.isNumeric(tmp)) {
                  connProps.setSoftMinEvictableIdleTimeMillis(Long.decode(tmp));
                  logValue(KEY_SOFT_MIN_EVICTABLE_IDLE_TIME_MILLIS, tmp);
              } else {
                  connProps.setSoftMinEvictableIdleTimeMillis(DEFAULT_SOFT_MIN_EVICTABLE_IDLE_TIME_MILLIS);
                  logDefault(KEY_SOFT_MIN_EVICTABLE_IDLE_TIME_MILLIS, tmp, "not numeric",
                          String.valueOf(DEFAULT_SOFT_MIN_EVICTABLE_IDLE_TIME_MILLIS));
              }
          } else {
              connProps.setSoftMinEvictableIdleTimeMillis(DEFAULT_SOFT_MIN_EVICTABLE_IDLE_TIME_MILLIS);
              logDefault(KEY_SOFT_MIN_EVICTABLE_IDLE_TIME_MILLIS,
                      String.valueOf(DEFAULT_SOFT_MIN_EVICTABLE_IDLE_TIME_MILLIS));
          }
  
          tmp = properties.get(KEY_LIFO);
          if (StringUtils.isNotEmpty(tmp)) {
              connProps.setLifo(Boolean.parseBoolean(tmp));
              logValue(KEY_LIFO, tmp);
          } else {
              connProps.setLifo(DEFAULT_LIFO);
              logDefault(KEY_LIFO, String.valueOf(DEFAULT_LIFO));
          }
  
          tmp = properties.get(KEY_AUTO_COMMIT);
          if (StringUtils.isNotEmpty(tmp)) {
              connProps.setDefaultAutoCommit(Boolean.parseBoolean(tmp));
              logValue(KEY_AUTO_COMMIT, tmp);
          } else {
              connProps.setDefaultAutoCommit(DEFAULT_AUTO_COMMIT);
              logDefault(KEY_AUTO_COMMIT, String.valueOf(DEFAULT_AUTO_COMMIT));
          }
  
          tmp = properties.get(KEY_READ_ONLY);
          if (StringUtils.isNotEmpty(tmp)) {
              connProps.setDefaultReadOnly(Boolean.parseBoolean(tmp));
              logValue(KEY_READ_ONLY, tmp);
          } else {
              connProps.setDefaultReadOnly(DEFAULT_READ_ONLY);
              logDefault(KEY_READ_ONLY, String.valueOf(DEFAULT_READ_ONLY));
          }
  
          tmp = properties.get(KEY_TRANSACTION_ISOLATION);
          if (StringUtils.isNotEmpty(tmp)) {
              if (StringUtils.isNumeric(tmp)) {
                  connProps.setDefaultTransactionIsolation(Integer.decode(tmp));
                  logValue(KEY_TRANSACTION_ISOLATION, tmp);
              } else {
                  connProps.setDefaultTransactionIsolation(DEFAULT_TRANSACTION_ISOLATION);
                  logDefault(KEY_TRANSACTION_ISOLATION, tmp, "not numeric",
                          String.valueOf(DEFAULT_TRANSACTION_ISOLATION));
              }
          } else {
              connProps.setDefaultTransactionIsolation(DEFAULT_TRANSACTION_ISOLATION);
              logDefault(KEY_TRANSACTION_ISOLATION, String.valueOf(DEFAULT_TRANSACTION_ISOLATION));
          }
  
          tmp = properties.get(KEY_CACHE_STATE);
          if (StringUtils.isNotEmpty(tmp)) {
              connProps.setCacheState(Boolean.parseBoolean(tmp));
              logValue(KEY_CACHE_STATE, tmp);
          } else {
              connProps.setCacheState(DEFAULT_CACHE_STATE);
              logDefault(KEY_CACHE_STATE, String.valueOf(DEFAULT_CACHE_STATE));
          }
  
          tmp = properties.get(KEY_VALIDATION_QUERY);
          if (StringUtils.isNotEmpty(tmp)) {
              connProps.setValidationQuery(tmp);
              logValue(KEY_VALIDATION_QUERY, tmp);
          } else {
              connProps.setValidationQuery(DEFAULT_VALIDATION_QUERY);
              logDefault(KEY_VALIDATION_QUERY, DEFAULT_VALIDATION_QUERY);
          }
  
          tmp = properties.get(KEY_MAX_CONN_LIFETIME_MILLIS);
          if (StringUtils.isNotEmpty(tmp)) {
              if (StringUtils.isNumeric(tmp)) {
                  connProps.setMaxConnLifetimeMillis(Long.decode(tmp));
                  logValue(KEY_MAX_CONN_LIFETIME_MILLIS, tmp);
              } else {
                  connProps.setMaxConnLifetimeMillis(DEFAULT_MAX_CONN_LIFETIME_MILLIS);
                  logDefault(KEY_MAX_CONN_LIFETIME_MILLIS, tmp, "not numeric",
                          String.valueOf(DEFAULT_MAX_CONN_LIFETIME_MILLIS));
              }
          } else {
              connProps.setMaxConnLifetimeMillis(DEFAULT_MAX_CONN_LIFETIME_MILLIS);
              logDefault(KEY_MAX_CONN_LIFETIME_MILLIS, String.valueOf(DEFAULT_MAX_CONN_LIFETIME_MILLIS));
          }
  
          // set the additional properties, preserving the originally provided properties
          // create a defensive copy of the map and all its properties
          // the code looks a little more complicated than a simple "putAll()", but it catches situations
          // where a Map is provided that supports null values (e.g. a HashMap) vs Map implementations
          // that do not (e.g. ConcurrentHashMap).
          final Map<String, String> tempMap = new ConcurrentHashMap<>();
          for (final Map.Entry<String, String> entry : properties.entrySet()) {
              final String key = entry.getKey();
              final String value = entry.getValue();
  
              if (value != null) {
                  tempMap.put(key, value);
              }
          }
          connProps.setAdditionalProperties(tempMap);
  
          return connProps;
      }