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.cache;
025
026import com.killcoding.cache.AbsCacheApi;
027import com.killcoding.tool.CommonTools;
028import java.util.Date;
029import java.util.Map;
030import java.util.concurrent.ConcurrentHashMap;
031import java.nio.file.Files;
032import java.io.File;
033import java.nio.file.Path;
034import java.util.stream.Stream;
035import java.util.Comparator;
036import java.nio.file.Paths;
037import com.killcoding.log.Logger;
038import com.killcoding.log.LoggerFactory;
039import com.killcoding.tool.FileTools;
040import java.util.Set;
041import java.util.Arrays;
042import java.util.List;
043import java.util.ArrayList;
044import java.util.concurrent.ExecutorService;
045import java.util.concurrent.Executors;
046import java.util.concurrent.Future;
047import java.text.SimpleDateFormat;
048import java.io.IOException;
049
050/**
051 * This cache class is save to hard disk
052 * */
053public class DiskCache extends AbsCacheApi {
054
055        private static final Logger log = LoggerFactory.getLogger(DiskCache.class);
056
057        private final long PID = ProcessHandle.current().pid();
058
059        private final static Map<String, String> CACHE_FILE = new ConcurrentHashMap<String, String>();
060        private final static Map<String, Date> CACHE_LIFE = new ConcurrentHashMap<String, Date>();
061
062        private File cacheTmpdir = null;
063        private static boolean isStartRemoveExpiredThread = false;
064        private static ExecutorService executor = null;
065
066    /**
067     * New a DiskCache object
068     * */
069        public DiskCache() {
070                super();
071                this.stored = false;
072                String tmpdir = System.getProperty("java.io.tmpdir");
073                SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmmssSSS");
074                String time = df.format(new Date());
075                String cacheTmp = String.format("%s/DiskCache/%s_%s_%s",tmpdir,PID,time,CommonTools.generateId(4));
076                cacheTmpdir = new File(cacheTmp);
077                try{
078                    FileTools.deleteDirectory(cacheTmpdir);
079                    cacheTmpdir.mkdirs();
080                }catch(Exception e){
081                    log.warn(e.getMessage(),e);
082                }
083        }
084        
085        /**
086         * New a DiskCache object
087         * @param stored - 'true' is force write cache
088         * */
089        public DiskCache(Boolean stored) {
090                super();
091                this.stored = stored;
092                String tmpdir = System.getProperty("java.io.tmpdir");
093                SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmmssSSS");
094                String time = df.format(new Date());
095                String cacheTmp = String.format("%s/DiskCache/%s_%s_%s",tmpdir,PID,time,CommonTools.generateId(4));
096                cacheTmpdir = new File(cacheTmp);
097                try{
098                    FileTools.deleteDirectory(cacheTmpdir);
099                    cacheTmpdir.mkdirs();
100                }catch(Exception e){
101                    log.warn(e.getMessage(),e);
102                }
103        }
104        
105        /**
106         * Shutdown cahee thread
107         * */
108        public synchronized void shutdown(){
109            removeAll();
110            if(executor != null)
111                executor.shutdown();
112        }
113
114    /**
115     * Start remove expired thread
116     * */
117        private synchronized void startRemoveExpiredThread() {
118            isStartRemoveExpiredThread = true;
119                executor = Executors.newFixedThreadPool(1);
120                executor.execute(new Runnable() {
121                        @Override
122                        public void run() {
123                                Thread.currentThread().setName("DiskCache");
124                                while(true){
125                                    
126                                    if(Thread.currentThread().isInterrupted()){
127                                       break; 
128                                    }
129                                    
130                                Set<String> names = CACHE_FILE.keySet();
131                                for (String name : names) {
132                                        Date current = new Date();
133                                        Date life = CACHE_LIFE.get(name);
134                                        if(life != null){
135                                                if (current.getTime() > life.getTime()) {
136                                                        remove(name);
137                                                        log.debug("Removed(Expired) -> {}",name);
138                                                }
139                                        }
140                                }
141                                try {
142                                        Thread.sleep(TIMER);
143                                } catch (InterruptedException e) {
144                                        log.warn(e);
145                                        Thread.currentThread().interrupted();
146                                }
147                                }
148                        }
149
150                });
151        }
152
153        @Override
154        synchronized boolean included(String name) {
155            if(stored){
156                return CACHE_FILE.containsKey(name);
157            }else{
158                if (CACHE_LIFE.containsKey(name)) {
159                        Date current = new Date();
160                        Date life = CACHE_LIFE.get(name);
161                        if(life != null){
162                                if (current.getTime() > life.getTime()) {
163                                        return false;
164                                } else {
165                                        return true;
166                                }
167                        }
168                        return false;
169                } else {
170                        return false;
171                }
172            }
173        }
174
175        @Override
176        synchronized List<String> getNames() {
177                Set<String> names = CACHE_FILE.keySet();
178                if (names != null) {
179                        return new ArrayList<String>(names);
180                } else {
181                        return null;
182                }
183        }
184
185        @Override
186        synchronized void removeAll() {
187                Set<String> names = CACHE_FILE.keySet();
188                for (String name : names) {
189                        remove(name);
190                }
191        }
192
193        @Override
194        synchronized void remove(String name) {
195                String cacheFileName = CACHE_FILE.get(name);
196                if (cacheFileName != null) {
197                        File cacheFile = new File(String.format("%s/%s", cacheTmpdir.getAbsolutePath(), cacheFileName));
198                        CACHE_LIFE.remove(name);
199                        CACHE_FILE.remove(name);
200                        if (cacheFile.exists())
201                                cacheFile.delete();
202                }
203        }
204
205        @Override
206        synchronized Object get(String name) {
207                try {
208                        if (included(name)) {
209                                String cacheFileName = CACHE_FILE.get(name);
210                                File cacheFile = new File(String.format("%s/%s", cacheTmpdir.getAbsolutePath(), cacheFileName));
211                                if(cacheFile != null && cacheFile.exists()){
212                                DataObject data = new DataObject(cacheFile.getAbsolutePath());
213                                return data.getData();
214                                }
215                                return null;
216                        } else {
217                                return null;
218                        }
219                } catch (Exception e) {
220                        log.warn(e);
221                        return null;
222                }
223        }
224
225        @Override
226        synchronized void set(String name, Object value, int lifeSeconds) {
227            if(value != null) {
228            
229            if(lifeSeconds <= 0){
230                log.debug("Skip use cache, key '{}' life is '{}s'",name,lifeSeconds);
231                return;    
232            }
233            
234                try {
235                        String cacheFileName = CommonTools.generateId(16);
236                        CACHE_FILE.put(name, cacheFileName);
237                        if(!stored){
238                            Date life = new Date(new Date().getTime() + lifeSeconds * 1000);
239                            CACHE_LIFE.put(name, life);
240                    }
241                        File cacheFile = new File(String.format("%s/%s", cacheTmpdir.getAbsolutePath(), cacheFileName));
242                        DataObject data = new DataObject(cacheFile.getAbsolutePath());
243                        data.setObject(value);
244                } catch (Exception e) {
245                        log.warn(e);
246                }
247                
248                if(!stored && !isStartRemoveExpiredThread)
249                    startRemoveExpiredThread();
250                    
251            }
252        }
253
254}