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("DefaultCache"); 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) return ; 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}