001/******************************************************************************* 002The MIT License (MIT) 003 004Copyright (c) 2024 KILLCODING.COM 005 006Permission is hereby granted, free of charge, to any person obtaining a copy 007of this software and associated documentation files (the "Software"), to deal 008in the Software without restriction, including without limitation the rights 009to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 010copies of the Software, and to permit persons to whom the Software is 011furnished to do so, subject to the following conditions: 012 013The above copyright notice and this permission notice shall be included in 014all copies or substantial portions of the Software. 015 016THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 017IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 018FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 019AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 020LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 021OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 022THE SOFTWARE. 023*****************************************************************************/ 024package com.killcoding.tool; 025 026import java.util.Properties; 027import java.util.regex.Pattern; 028import java.util.regex.Matcher; 029import java.util.Set; 030import java.util.Map; 031import java.util.Enumeration; 032import java.util.HashMap; 033import java.util.List; 034import java.util.ArrayList; 035import java.util.Collections; 036import java.util.Arrays; 037import com.killcoding.log.Logger; 038import java.io.StringReader; 039import java.io.Reader; 040import java.io.IOException; 041import java.io.InputStream; 042import java.io.InputStreamReader; 043import java.io.File; 044import java.io.FileInputStream; 045import java.util.Collection; 046import java.util.concurrent.ExecutorService; 047import java.util.concurrent.Executors; 048import java.nio.file.Files; 049import java.nio.file.Path; 050import java.nio.file.LinkOption; 051import java.nio.file.Paths; 052import java.net.URI; 053import java.text.SimpleDateFormat; 054import java.util.Date; 055import java.text.ParseException; 056import com.killcoding.file.BaseFile; 057import java.util.concurrent.CopyOnWriteArrayList; 058 059public class ConfigProperties extends Properties { 060 061 public static final String ENV_VAR_REGEX = "[\\$%#]{1}\\{?([\\.\\w-]+)(:{1}([^\\}]+)){0,1}\\}"; 062 063 public static String DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss.SSS"; 064 065 public static Long AUTO_LOAD_TIMER = 5000L; 066 private static final List<Runnable> AUTO_LOAD_CP = new CopyOnWriteArrayList<Runnable>(); 067 068 private static ConfigCustomLoad CUSTOMLOAD = null; 069 070 protected static ExecutorService executor = null; 071 072 private String parseContentRaw = null; 073 074 public ConfigProperties(int arg0) { 075 super(arg0); 076 } 077 078 public ConfigProperties(Properties arg0) { 079 super(arg0); 080 } 081 082 public ConfigProperties() { 083 super(); 084 } 085 086 private void addAutoLoad(Runnable cpRun) { 087 AUTO_LOAD_CP.add(cpRun); 088 startAutoLoad(); 089 } 090 091 private static synchronized void startAutoLoad() { 092 if (executor == null) { 093 executor = Executors.newFixedThreadPool(1); 094 executor.execute(new Runnable() { 095 @Override 096 public void run() { 097 while(!Thread.currentThread().isInterrupted()){ 098 for (Runnable cpRun : AUTO_LOAD_CP) { 099 cpRun.run(); 100 } 101 try { 102 Thread.sleep(getAutoLoadTimer()); 103 } catch (InterruptedException e) { 104 Logger.systemError(ConfigProperties.class, e.getMessage(), e); 105 break; 106 } 107 } 108 } 109 }); 110 } 111 } 112 113 private static Long getAutoLoadTimer(){ 114 return AUTO_LOAD_TIMER; 115 } 116 117 public static void setConfigCustom(ConfigCustomLoad customLoad){ 118 ConfigProperties.CUSTOMLOAD = customLoad; 119 } 120 121 private static boolean loadConfigCustom(File file,ConfigProperties target){ 122 if(ConfigProperties.CUSTOMLOAD != null){ 123 ConfigProperties.CUSTOMLOAD.load(file,target); 124 return true; 125 } 126 return false; 127 } 128 129 public synchronized void load(File file, Runnable updatedRunnable) throws IOException { 130 this.load(new FileInputStream(file)); 131 final ConfigProperties the = this; 132 final Runnable cpRun = new Runnable() { 133 @Override 134 public void run() { 135 Thread.currentThread().setName(String.format("AutoLoad-%s", file.getName())); 136 try { 137 ConfigProperties target = new ConfigProperties(); 138 if(Files.exists(Paths.get(file.getAbsolutePath()))){ 139 target.load(new FileInputStream(file)); 140 boolean isUseCustom = false; 141 boolean haveCustomLoad = loadConfigCustom(file,the); 142 if(haveCustomLoad){ 143 isUseCustom = ConfigProperties.CUSTOMLOAD.isUseCustom(); 144 boolean isUpdated = ConfigProperties.CUSTOMLOAD.isUpdated(); 145 if(isUpdated){ 146 Logger.systemMark(ConfigProperties.class, "CustomUpdated."); 147 if (updatedRunnable != null) 148 updatedRunnable.run(); 149 } 150 } 151 if(!isUseCustom){ 152 boolean same = ConfigProperties.compareProperties(the, target); 153 if (!same) { 154 the.load(new FileInputStream(file)); 155 Logger.systemMark(ConfigProperties.class, "Updated"); 156 Logger.systemInfo(ConfigProperties.class, the.parseContentRaw); 157 158 if (updatedRunnable != null) 159 updatedRunnable.run(); 160 } 161 } 162 } 163 } catch (Exception e) { 164 Logger.systemError(ConfigProperties.class, e.getMessage(), e); 165 } 166 } 167 }; 168 addAutoLoad(cpRun); 169 } 170 171 public void load(File file) throws IOException { 172 load(new FileInputStream(file)); 173 } 174 175 @Override 176 public void load(InputStream inputStream) throws IOException { 177 try { 178 Reader reader = new InputStreamReader(inputStream,BaseFile.CHARSET); 179 load(reader); 180 } finally { 181 if (inputStream != null) 182 inputStream.close(); 183 } 184 } 185 186 @Override 187 public void load(Reader reader) throws IOException { 188 try { 189 int readChar; 190 StringBuffer sbf = new StringBuffer(); 191 while ((readChar = reader.read()) != -1) { 192 sbf.append((char) readChar); 193 } 194 this.parseContentRaw = parseContent(sbf.toString()); 195 super.load(new StringReader(this.parseContentRaw)); 196 } finally { 197 if (reader != null) 198 reader.close(); 199 } 200 } 201 202 public ConfigProperties putAndReturnSlef(String key, Object value) { 203 if (isBlank(value)) { 204 put(key, ""); 205 } else if (value instanceof Map) { 206 Map<String, Object> subMap = conver(key, (Map<String, Object>) value); 207 putAllAndReturnSlef(subMap); 208 } else if (value instanceof List) { 209 List<Object> list = (List<Object>) value; 210 int size = list.size(); 211 for (int i = 0; i < size; i++) { 212 Object item = list.get(i); 213 String newKey = String.format("%s[%s]", key, i); 214 putAndReturnSlef(newKey, item); 215 } 216 } else if (value instanceof Object[]) { 217 List<Object> list = Arrays.asList((Object[]) value); 218 int size = list.size(); 219 for (int i = 0; i < size; i++) { 220 Object item = list.get(i); 221 String newKey = String.format("%s[%s]", key, i); 222 putAndReturnSlef(newKey, item); 223 } 224 } else if (value instanceof String) { 225 put(key, value); 226 } else { 227 put(key, value.toString()); 228 } 229 return this; 230 } 231 232 public ConfigProperties removeAndReturnSlef(String key) { 233 this.remove(key); 234 return this; 235 } 236 237 public ConfigProperties putAllAndReturnSlef(Map<String, Object> map) { 238 Set<String> keys = map.keySet(); 239 for (String key : keys) { 240 Object value = map.get(key); 241 putAndReturnSlef(key, value); 242 } 243 return this; 244 } 245 246 public void removeAllNullValues() { 247 Set<Object> keys = this.keySet(); 248 for (Object key : keys) { 249 Object value = get(key); 250 boolean blank = isBlank(value); 251 if (blank) { 252 this.remove(key); 253 continue; 254 } 255 } 256 } 257 258 private Map<String, Object> conver(String parentKey, Map<String, Object> map) { 259 Map<String, Object> subMap = new HashMap<String, Object>(); 260 Set<String> keys = map.keySet(); 261 for (String key : keys) { 262 String newKey = String.format("%s.%s", parentKey, key); 263 Object value = map.get(key); 264 subMap.put(newKey, value); 265 } 266 return subMap; 267 } 268 269 public Boolean getBoolean(String key) { 270 return getBoolean(key, null); 271 } 272 273 public Boolean getBoolean(String key, Boolean defaultValue) { 274 Object originValue = get(key); 275 276 if(originValue == null) return defaultValue; 277 278 if(originValue instanceof Boolean) return (Boolean)originValue; 279 280 String o = getString(key); 281 if (isBlank(o)) { 282 return defaultValue; 283 } 284 return Boolean.parseBoolean(o.trim()); 285 } 286 287 public List<String> getArray(String key) { 288 List<String> defaultValue = null; 289 List<String> v = getArray(key, defaultValue); 290 if (v == null) { 291 String sv = getString(key); 292 if (sv == null) 293 return new ArrayList<String>(); 294 295 return new ArrayList<String>(Arrays.asList(new String[] { sv })); 296 } else { 297 return v; 298 } 299 } 300 301 public List<String> getArray(String key, String[] defaultValue) { 302 return getArray(key, Arrays.asList(defaultValue)); 303 } 304 305 public List<String> getArray(String key, List<String> defaultValue) { 306 String[] list = null; 307 Map<Integer, String> listMap = new HashMap<Integer, String>(); 308 List<Integer> listIndex = new ArrayList<Integer>(); 309 Set<Object> keys = keySet(); 310 for (Object _key : keys) { 311 String format = String.format("^%s\\[(\\w+)\\]$", key); 312 Pattern pattern = Pattern.compile(format); 313 Matcher matcher = pattern.matcher(_key.toString()); 314 if (matcher.find()) { 315 Integer index = Integer.parseInt(matcher.group(1)); 316 String value = getString(_key.toString()); 317 listMap.put(index, value); 318 listIndex.add(index); 319 } 320 } 321 int listIndexSize = listIndex.size(); 322 if (listIndexSize > 0) { 323 Collections.sort(listIndex); 324 list = new String[listIndex.get(listIndexSize - 1) + 1]; 325 for (int i = 0; i < listIndexSize; i++) { 326 int index = listIndex.get(i); 327 String value = listMap.get(index); 328 list[index] = isBlank(value) ? null : value; 329 } 330 return Arrays.asList(list); 331 } else { 332 return defaultValue; 333 } 334 335 } 336 337 public String getString(String key) { 338 return getString(key, null); 339 } 340 341 public String getString(String key, String defaultValue) { 342 String o = getProperty(key); 343 if (isBlank(o)) { 344 return defaultValue; 345 } 346 return o.trim(); 347 } 348 349 // public String getString(String key, String defaultValue) { 350 // String o = getProperty(key); 351 // if(o == null) 352 // return o; 353 354 // String originValue = o.trim(); 355 // Pattern p = Pattern.compile(ENV_VAR_REGEX); 356 // Matcher m = p.matcher(originValue); 357 // boolean findEnvFormat = m.find(); 358 // if(findEnvFormat){ 359 // String envVar = m.group(1); 360 // String envDefValue = m.group(3); 361 // String envValue = getSysEnv(envVar); 362 // if (!isBlank(envValue)) { 363 // return envValue; 364 // } 365 // if (!isBlank(envDefValue)) { 366 // return envDefValue; 367 // } 368 // return defaultValue; 369 // }else{ 370 // if (isBlank(originValue)) { 371 // return defaultValue; 372 // } 373 // return originValue.trim(); 374 // } 375 // } 376 377 public Short getShort(String key) { 378 return getShort(key, null); 379 } 380 381 public Short getShort(String key, Short defaultValue) { 382 Object originValue = get(key); 383 384 if(originValue == null) return defaultValue; 385 386 if(originValue instanceof Number) return Short.parseShort(originValue + ""); 387 388 String o = getString(key); 389 if (isBlank(o)) { 390 return defaultValue; 391 } 392 return Short.parseShort(o.trim()); 393 } 394 395 public Integer getInteger(String key) { 396 return getInteger(key, null); 397 } 398 399 public Integer getInteger(String key, Integer defaultValue) { 400 Object originValue = get(key); 401 402 if(originValue == null) return defaultValue; 403 404 if(originValue instanceof Number) return Integer.parseInt(originValue + ""); 405 406 String o = getString(key); 407 if (isBlank(o)) { 408 return defaultValue; 409 } 410 return Integer.parseInt(o.trim()); 411 } 412 413 public Long getLong(String key) { 414 return getLong(key, null); 415 } 416 417 public Long getLong(String key, Long defaultValue) { 418 Object originValue = get(key); 419 420 if(originValue == null) return defaultValue; 421 422 if(originValue instanceof Number) return Long.parseLong(originValue + ""); 423 424 String o = getString(key); 425 if (isBlank(o)) { 426 return defaultValue; 427 } 428 return Long.parseLong(o.trim()); 429 } 430 431 public Float getFloat(String key) { 432 return getFloat(key, null); 433 } 434 435 public Float getFloat(String key, Float defaultValue) { 436 Object originValue = get(key); 437 438 if(originValue == null) return defaultValue; 439 440 if(originValue instanceof Number) return Float.parseFloat(originValue + ""); 441 442 String o = getString(key); 443 if (isBlank(o)) { 444 return defaultValue; 445 } 446 return Float.parseFloat(o.trim()); 447 } 448 449 public Double getDouble(String key) { 450 return getDouble(key, null); 451 } 452 453 public Double getDouble(String key, Double defaultValue) { 454 Object originValue = get(key); 455 456 if(originValue == null) return defaultValue; 457 458 if(originValue instanceof Number) return Double.parseDouble(originValue + ""); 459 460 String o = getString(key); 461 if (isBlank(o)) { 462 return defaultValue; 463 } 464 return Double.parseDouble(o.trim()); 465 } 466 467 public Long getSeconds(String key) { 468 return getSeconds(key, null); 469 } 470 471 public Long getSeconds(String key, Long defaultValue) { 472 return parseTimeToSec(getString(key, null), defaultValue); 473 } 474 475 public Long getMilliSeconds(String key) { 476 return getMilliSeconds(key, null); 477 } 478 479 public Long getMilliSeconds(String key, Long defaultValue) { 480 return parseTimeToMs(getString(key, null), defaultValue); 481 } 482 483 public Long getFileSize(String key, Long defaultValue) { 484 return parseFileSize(getString(key, null), defaultValue); 485 } 486 487 public Date getDateTime(String key, String defaultValue) { 488 return parseDateTime(getString(key, null), defaultValue); 489 } 490 491 public Date getDateTime(String key) { 492 return getDateTime(key,null); 493 } 494 495 public ConfigProperties getConfigProperties(String key) { 496 Map<String, Object> map = getMap(key); 497 if (map != null) { 498 ConfigProperties cp = new ConfigProperties(); 499 cp.putAllAndReturnSlef(map); 500 return cp; 501 } 502 return null; 503 } 504 505 public ConfigProperties getConfigProperties(String key, ConfigProperties defaultValue) { 506 Map<String, Object> map = getMap(key); 507 if (map != null) { 508 ConfigProperties cp = new ConfigProperties(); 509 cp.putAllAndReturnSlef(map); 510 return cp; 511 } 512 return defaultValue; 513 } 514 515 public Map<String, Object> getMap(String key) { 516 return getMap(key, null); 517 } 518 519 public Map<String, Object> getMap(String key, Map<String, Object> defaultValue) { 520 Map<String, Object> cp = null; 521 Set<Object> keys = this.keySet(); 522 for (Object keyObject : keys) { 523 String _key = keyObject.toString(); 524 boolean isMap = _key.startsWith(String.format("%s.", key)); 525 if (isMap) { 526 if (cp == null) 527 cp = new HashMap<String, Object>(); 528 529 String mapKey = _key.replaceFirst(String.format("^%s\\.", key), ""); 530 cp.put(mapKey, this.getString(_key, null)); 531 } 532 } 533 return isBlank(cp) ? defaultValue : cp; 534 } 535 536 public String getContentRaw(){ 537 return this.parseContentRaw; 538 } 539 540 public static boolean isBlank(Object o) { 541 return CommonTools.isBlank(o); 542 } 543 544 public static Long parseTimeToSec(Object objectValue, Long defaultValue) { 545 546 if (objectValue == null) 547 return defaultValue; 548 549 String stringValue = null; 550 if (objectValue instanceof String) { 551 stringValue = objectValue.toString().trim(); 552 } else { 553 return Long.parseLong(objectValue.toString().trim()); 554 } 555 556 Long value = null; 557 try { 558 Pattern pattern = Pattern.compile("^([-\\+]{0,1}[0-9]+)(((?i)ms|s|m|h|d|w){1})$"); 559 Matcher matcher = pattern.matcher(stringValue); 560 if (matcher.find()) { 561 Long originValue = Long.parseLong(matcher.group(1)); 562 String unit = matcher.group(2).toLowerCase(); 563 if (unit.equals("ms")) { 564 value = originValue / 1000; 565 } 566 if (unit.equals("s")) { 567 value = originValue; 568 } 569 if (unit.equals("m")) { 570 value = originValue * 60; 571 } 572 if (unit.equals("h")) { 573 value = originValue * 60 * 60; 574 } 575 if (unit.equals("d")) { 576 value = originValue * 60 * 60 * 24; 577 } 578 if (unit.equals("w")) { 579 value = originValue * 60 * 60 * 24 * 7; 580 } 581 } else { 582 Logger.systemMark(ConfigProperties.class, 583 "Missing unit(ms|s|m|h|d|w) '{}' will using default value '{}', (e.g. {}ms,{}s,...).", 584 stringValue, defaultValue, stringValue, stringValue); 585 } 586 return value == null ? defaultValue : value; 587 } catch (Exception e) { 588 Logger.systemError(ConfigProperties.class, e); 589 return null; 590 } 591 } 592 593 public static Long parseTimeToMs(Object objectValue, Long defaultValue) { 594 595 if (objectValue == null) 596 return defaultValue; 597 598 String stringValue = null; 599 if (objectValue instanceof String) { 600 stringValue = objectValue.toString().trim(); 601 } else { 602 return Long.parseLong(objectValue.toString().trim()); 603 } 604 605 Long value = null; 606 try { 607 Pattern pattern = Pattern.compile("^([-\\+]{0,1}[0-9]+)(((?i)ms|s|m|h|d|w){1})$"); 608 Matcher matcher = pattern.matcher(stringValue); 609 if (matcher.find()) { 610 Long originValue = Long.parseLong(matcher.group(1)); 611 String unit = matcher.group(2).toLowerCase(); 612 if (unit.equals("ms")) { 613 value = originValue; 614 } 615 if (unit.equals("s")) { 616 value = originValue * 1000; 617 } 618 if (unit.equals("m")) { 619 value = originValue * 60 * 1000; 620 } 621 if (unit.equals("h")) { 622 value = originValue * 60 * 60 * 1000; 623 } 624 if (unit.equals("d")) { 625 value = originValue * 60 * 60 * 1000 * 24; 626 } 627 if (unit.equals("w")) { 628 value = originValue * 60 * 60 * 1000 * 24 * 7; 629 } 630 } else { 631 Logger.systemMark(ConfigProperties.class, 632 "Missing unit(ms|s|m|h|d|w) '{}' will using default value '{}', (e.g. {}ms,{}s,...).", 633 stringValue, defaultValue, stringValue, stringValue); 634 } 635 return value == null ? defaultValue : value; 636 } catch (Exception e) { 637 Logger.systemError(ConfigProperties.class, e); 638 return null; 639 } 640 } 641 642 public static Date parseDateTime(String objectValue, String defaultValue) { 643 SimpleDateFormat sf = new SimpleDateFormat(DATETIME_PATTERN); 644 try{ 645 if(CommonTools.isBlank(objectValue)){ 646 if(CommonTools.isBlank(defaultValue)) return null; 647 648 if(!CommonTools.isBlank(defaultValue)) return sf.parse(defaultValue); 649 } 650 651 return sf.parse(objectValue); 652 }catch(Exception e){ 653 Logger.systemError(ConfigProperties.class, e); 654 return null; 655 } 656 } 657 658 public static Long parseFileSize(Object objectValue, Long defaultValue) { 659 660 if (objectValue == null) 661 return defaultValue; 662 663 String stringValue = null; 664 if (objectValue instanceof String) { 665 stringValue = objectValue.toString().trim(); 666 } else { 667 return Long.parseLong(objectValue.toString().trim()); 668 } 669 670 Long value = null; 671 try { 672 Pattern pattern = Pattern.compile("^([-\\+]{0,1}[0-9]+)(((?i)b|kb|mb|gb|tb){1})$"); 673 Matcher matcher = pattern.matcher(stringValue); 674 if (matcher.find()) { 675 Long originValue = Long.parseLong(matcher.group(1)); 676 String unit = matcher.group(2).toLowerCase(); 677 if (unit.equals("b")) { 678 value = originValue; 679 } 680 if (unit.equals("kb")) { 681 value = originValue * 1024; 682 } 683 if (unit.equals("mb")) { 684 value = originValue * 1024 * 1024; 685 } 686 if (unit.equals("gb")) { 687 value = originValue * 1024 * 1024 * 1024; 688 } 689 if (unit.equals("tb")) { 690 value = originValue * 1024 * 1024 * 1024 * 1024; 691 } 692 } else { 693 Logger.systemMark(ConfigProperties.class, 694 "Missing unit(b|kb|mb|gb|tb) '{}' will using default value '{}', (e.g. {}mb,{}kb,...).", 695 stringValue, defaultValue, stringValue, stringValue); 696 } 697 return value == null ? defaultValue : value; 698 } catch (Exception e) { 699 Logger.systemError(ConfigProperties.class, e); 700 return null; 701 } 702 } 703 704 public static String getSysEnv(String envKey) { 705 return System.getenv(envKey) == null ? System.getProperty(envKey) : System.getenv(envKey); 706 } 707 708 public static String getEnv(String originValue) { 709 if (originValue == null) 710 return null; 711 712 Pattern p = Pattern.compile(ENV_VAR_REGEX); 713 Matcher m = p.matcher(originValue.trim()); 714 boolean findEnvFormat = m.find(); 715 if (findEnvFormat) { 716 String envVar = m.group(1); 717 String envDefValue = m.group(3); 718 String envValue = getSysEnv(envVar); 719 if (!isBlank(envValue)) { 720 return envValue; 721 } 722 if (!isBlank(envDefValue)) { 723 return envDefValue; 724 } 725 return null; 726 } 727 return originValue; 728 } 729 730 public static String parseContent(String content) { 731 return parseContent(content, ""); 732 } 733 734 public static String parseContent(String content, String replaceNullChar) { 735 Pattern p = Pattern.compile(ENV_VAR_REGEX, Pattern.DOTALL | Pattern.MULTILINE); 736 StringBuffer sb = new StringBuffer(); 737 Matcher matcher = p.matcher(content); 738 while (matcher.find()) { 739 String envValue = ConfigProperties.getEnv(matcher.group()); 740 envValue = (envValue == null ? replaceNullChar : envValue); 741 matcher.appendReplacement(sb, envValue); 742 } 743 matcher.appendTail(sb); 744 return sb.toString(); 745 } 746 747 public static boolean compareProperties(ConfigProperties props1, ConfigProperties props2) { 748 749 if (props1 == null || props2 == null) 750 return false; 751 752 return props1.parseContentRaw.equals(props2.parseContentRaw); 753 } 754 755}