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