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