[AAF-21] Initial code import
[aaf/authz.git] / authz-core / src / main / java / com / att / cssa / rserv / CachingFileAccess.java
1 /*******************************************************************************\r
2  * ============LICENSE_START====================================================\r
3  * * org.onap.aai\r
4  * * ===========================================================================\r
5  * * Copyright © 2017 AT&T Intellectual Property. All rights reserved.\r
6  * * Copyright © 2017 Amdocs\r
7  * * ===========================================================================\r
8  * * Licensed under the Apache License, Version 2.0 (the "License");\r
9  * * you may not use this file except in compliance with the License.\r
10  * * You may obtain a copy of the License at\r
11  * * \r
12  *  *      http://www.apache.org/licenses/LICENSE-2.0\r
13  * * \r
14  *  * Unless required by applicable law or agreed to in writing, software\r
15  * * distributed under the License is distributed on an "AS IS" BASIS,\r
16  * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
17  * * See the License for the specific language governing permissions and\r
18  * * limitations under the License.\r
19  * * ============LICENSE_END====================================================\r
20  * *\r
21  * * ECOMP is a trademark and service mark of AT&T Intellectual Property.\r
22  * *\r
23  ******************************************************************************/\r
24 package com.att.cssa.rserv;\r
25 \r
26 \r
27 import java.io.File;\r
28 import java.io.FileInputStream;\r
29 import java.io.FileNotFoundException;\r
30 import java.io.FileOutputStream;\r
31 import java.io.FileReader;\r
32 import java.io.IOException;\r
33 import java.io.OutputStream;\r
34 import java.io.Writer;\r
35 import java.nio.ByteBuffer;\r
36 import java.nio.channels.FileChannel;\r
37 import java.util.ArrayList;\r
38 import java.util.Collections;\r
39 import java.util.Date;\r
40 import java.util.HashSet;\r
41 import java.util.Map;\r
42 import java.util.Map.Entry;\r
43 import java.util.NavigableMap;\r
44 import java.util.Set;\r
45 import java.util.Timer;\r
46 import java.util.TimerTask;\r
47 import java.util.TreeMap;\r
48 import java.util.concurrent.ConcurrentSkipListMap;\r
49 \r
50 import javax.servlet.http.HttpServletRequest;\r
51 import javax.servlet.http.HttpServletResponse;\r
52 \r
53 import com.att.aft.dme2.internal.jetty.http.HttpStatus;\r
54 import com.att.inno.env.Env;\r
55 import com.att.inno.env.EnvJAXB;\r
56 import com.att.inno.env.LogTarget;\r
57 import com.att.inno.env.Store;\r
58 import com.att.inno.env.TimeTaken;\r
59 import com.att.inno.env.Trans;\r
60 /*\r
61  * CachingFileAccess\r
62  * \r
63  *  \r
64  */\r
65 public class CachingFileAccess<TRANS extends Trans> extends HttpCode<TRANS, Void> {\r
66         public static void setEnv(Store store, String[] args) {\r
67                 for(int i=0;i<args.length-1;i+=2) { // cover two parms required for each \r
68                         if(CFA_WEB_DIR.equals(args[i])) {\r
69                                 store.put(store.staticSlot(CFA_WEB_DIR), args[i+1]); \r
70                         } else if(CFA_CACHE_CHECK_INTERVAL.equals(args[i])) {\r
71                                 store.put(store.staticSlot(CFA_CACHE_CHECK_INTERVAL), Long.parseLong(args[i+1]));\r
72                         } else if(CFA_MAX_SIZE.equals(args[i])) {\r
73                                 store.put(store.staticSlot(CFA_MAX_SIZE), Integer.parseInt(args[i+1]));\r
74                         }\r
75                 }\r
76         }\r
77         \r
78         private static String MAX_AGE = "max-age=3600"; // 1 hour Caching\r
79         private final Map<String,String> typeMap;\r
80         private final NavigableMap<String,Content> content;\r
81         private final Set<String> attachOnly;\r
82         private final static String WEB_DIR_DEFAULT = "theme";\r
83         public final static String CFA_WEB_DIR = "CFA_WebPath";\r
84         // when to re-validate from file\r
85         // Re validating means comparing the Timestamp on the disk, and seeing it has changed.  Cache is not marked\r
86         // dirty unless file has changed, but it still makes File IO, which for some kinds of cached data, i.e. \r
87         // deployed GUI elements is unnecessary, and wastes time.\r
88         // This parameter exists to cover the cases where data can be more volatile, so the user can choose how often the\r
89         // File IO will be accessed, based on probability of change.  "0", of course, means, check every time.\r
90         private final static String CFA_CACHE_CHECK_INTERVAL = "CFA_CheckIntervalMS";\r
91         private final static String CFA_MAX_SIZE = "CFA_MaxSize"; // Cache size limit\r
92         private final static String CFA_CLEAR_COMMAND = "CFA_ClearCommand";\r
93 \r
94         // Note: can be null without a problem, but included\r
95         // to tie in with existing Logging.\r
96         public LogTarget logT = null;\r
97         public long checkInterval; // = 600000L; // only check if not hit in 10 mins by default\r
98         public int maxItemSize; // = 512000; // max file 500k\r
99         private Timer timer;\r
100         private String web_path;\r
101         // A command key is set in the Properties, preferably changed on deployment.\r
102         // it is compared at the beginning of the path, and if so, it is assumed to issue certain commands\r
103         // It's purpose is to protect, to some degree the command, even though it is HTTP, allowing \r
104         // local batch files to, for instance, clear caches on resetting of files.\r
105         private String clear_command;\r
106         \r
107         public CachingFileAccess(EnvJAXB env, String ... args) {\r
108                 super(null,"Caching File Access");\r
109                 setEnv(env,args);\r
110                 content = new ConcurrentSkipListMap<String,Content>(); // multi-thread changes possible\r
111 \r
112                 attachOnly = new HashSet<String>();     // short, unchanged\r
113 \r
114                 typeMap = new TreeMap<String,String>(); // Structure unchanged after Construction\r
115                 typeMap.put("ico","image/icon");\r
116                 typeMap.put("html","text/html");\r
117                 typeMap.put("css","text/css");\r
118                 typeMap.put("js","text/javascript");\r
119                 typeMap.put("txt","text/plain");\r
120                 typeMap.put("xml","text/xml");\r
121                 typeMap.put("xsd","text/xml");\r
122                 attachOnly.add("xsd");\r
123                 typeMap.put("crl", "application/x-pkcs7-crl");\r
124                 typeMap.put("appcache","text/cache-manifest");\r
125 \r
126                 typeMap.put("json","text/json");\r
127                 typeMap.put("ogg", "audio/ogg");\r
128                 typeMap.put("jpg","image/jpeg");\r
129                 typeMap.put("gif","image/gif");\r
130                 typeMap.put("png","image/png");\r
131                 typeMap.put("svg","image/svg+xml");\r
132                 typeMap.put("jar","application/x-java-applet");\r
133                 typeMap.put("jnlp", "application/x-java-jnlp-file");\r
134                 typeMap.put("class", "application/java");\r
135                 \r
136                 timer = new Timer("Caching Cleanup",true);\r
137                 timer.schedule(new Cleanup(content,500),60000,60000);\r
138                 \r
139                 // Property params\r
140                 web_path = env.getProperty(CFA_WEB_DIR,WEB_DIR_DEFAULT);\r
141                 Object obj;\r
142                 obj = env.get(env.staticSlot(CFA_CACHE_CHECK_INTERVAL),600000L);  // Default is 10 mins\r
143                 if(obj instanceof Long) {checkInterval=(Long)obj;\r
144                 } else {checkInterval=Long.parseLong((String)obj);}\r
145                 \r
146                 obj = env.get(env.staticSlot(CFA_MAX_SIZE), 512000);    // Default is max file 500k\r
147                 if(obj instanceof Integer) {maxItemSize=(Integer)obj;\r
148                 } else {maxItemSize =Integer.parseInt((String)obj);}\r
149                         \r
150                 clear_command = env.getProperty(CFA_CLEAR_COMMAND,null);\r
151         }\r
152 \r
153         \r
154 \r
155         @Override\r
156         public void handle(TRANS trans, HttpServletRequest req, HttpServletResponse resp) throws IOException {\r
157                 String key = pathParam(req, ":key");\r
158                 if(key.equals(clear_command)) {\r
159                         String cmd = pathParam(req,":cmd");\r
160                         resp.setHeader("Content-type",typeMap.get("txt"));\r
161                         if("clear".equals(cmd)) {\r
162                                 content.clear();\r
163                                 resp.setStatus(HttpStatus.OK_200);\r
164                         } else {\r
165                                 resp.setStatus(HttpStatus.BAD_REQUEST_400);\r
166                         }\r
167                         return;\r
168                 }\r
169                 Content c = load(logT , web_path,key, null, checkInterval);\r
170                 if(c.attachmentOnly) {\r
171                         resp.setHeader("Content-disposition", "attachment");\r
172                 }\r
173                 c.write(resp.getOutputStream());\r
174                 c.setHeader(resp);\r
175                 trans.checkpoint(req.getPathInfo());\r
176         }\r
177 \r
178 \r
179         public String webPath() {\r
180                 return web_path;\r
181         }\r
182         \r
183         /**\r
184          * Reset the Cleanup size and interval\r
185          * \r
186          * The size and interval when started are 500 items (memory size unknown) checked every minute in a background thread.\r
187          * \r
188          * @param size\r
189          * @param interval\r
190          */\r
191         public void cleanupParams(int size, long interval) {\r
192                 timer.cancel();\r
193                 timer.schedule(new Cleanup(content,size), interval, interval);\r
194         }\r
195         \r
196 \r
197         \r
198         /**\r
199          * Load a file, first checking cache\r
200          * \r
201          * \r
202          * @param logTarget - logTarget can be null (won't log)\r
203          * @param dataRoot - data root storage directory\r
204          * @param key - relative File Path\r
205          * @param mediaType - what kind of file is it.  If null, will check via file extension\r
206          * @param timeCheck - "-1" will take system default - Otherwise, will compare "now" + timeCheck(Millis) before looking at File mod\r
207          * @return\r
208          * @throws IOException\r
209          */\r
210         public Content load(LogTarget logTarget, String dataRoot, String key, String mediaType, long _timeCheck) throws IOException {\r
211             long timeCheck = _timeCheck;\r
212                 if(timeCheck<0) {\r
213                         timeCheck=checkInterval; // if time < 0, then use default\r
214                 }\r
215                 String fileName = dataRoot + '/' + key;\r
216                 Content c = content.get(key);\r
217                 long systime = System.currentTimeMillis(); \r
218                 File f=null;\r
219                 if(c!=null) {\r
220                         // Don't check every hit... only after certain time value\r
221                         if(c.date < systime + timeCheck) {\r
222                                 f = new File(fileName);\r
223                                 if(f.lastModified()>c.date) {\r
224                                         c=null;\r
225                                 }\r
226                         }\r
227                 }\r
228                 if(c==null) {   \r
229                         if(logTarget!=null) {\r
230                                 logTarget.log("File Read: ",key);\r
231                         }\r
232                         \r
233                         if(f==null){\r
234                                 f = new File(fileName);\r
235                         }\r
236 \r
237                         boolean cacheMe;\r
238                         if(f.exists()) {\r
239                                 if(f.length() > maxItemSize) {\r
240                                         c = new DirectFileContent(f);\r
241                                         cacheMe = false;\r
242                                 } else {\r
243                                         c = new CachedContent(f);\r
244                                         cacheMe = checkInterval>0;\r
245                                 }\r
246                                 \r
247                                 if(mediaType==null) { // determine from file Ending\r
248                                         int idx = key.lastIndexOf('.');\r
249                                         String subkey = key.substring(++idx);\r
250                                         if((c.contentType = idx<0?null:typeMap.get(subkey))==null) {\r
251                                                 // if nothing else, just set to default type...\r
252                                                 c.contentType = "application/octet-stream";\r
253                                         }\r
254                                         c.attachmentOnly = attachOnly.contains(subkey);\r
255                                 } else {\r
256                                         c.contentType=mediaType;\r
257                                         c.attachmentOnly = false;\r
258                                 }\r
259                                 \r
260                                 c.date = f.lastModified();\r
261                                 \r
262                                 if(cacheMe) {\r
263                                         content.put(key, c);\r
264                                 }\r
265                         } else {\r
266                                 c=NULL;\r
267                         }\r
268                 } else {\r
269                         if(logTarget!=null)logTarget.log("Cache Read: ",key);\r
270                 }\r
271 \r
272                 // refresh hit time\r
273                 c.access = systime;\r
274                 return c;\r
275         }\r
276         \r
277         public Content loadOrDefault(Trans trans, String targetDir, String targetFileName, String sourcePath, String mediaType) throws IOException {\r
278                 try {\r
279                         return load(trans.info(),targetDir,targetFileName,mediaType,0);\r
280                 } catch(FileNotFoundException e) {\r
281                         String targetPath = targetDir + '/' + targetFileName;\r
282                         TimeTaken tt = trans.start("File doesn't exist; copy " + sourcePath + " to " + targetPath, Env.SUB);\r
283                         try {\r
284                                 FileInputStream sourceFIS = new FileInputStream(sourcePath);\r
285                                 FileChannel sourceFC = sourceFIS.getChannel();\r
286                                 File targetFile = new File(targetPath);\r
287                                 targetFile.getParentFile().mkdirs(); // ensure directory exists\r
288                                 FileOutputStream targetFOS = new FileOutputStream(targetFile);\r
289                                 try {\r
290                                         ByteBuffer bb = ByteBuffer.allocate((int)sourceFC.size());\r
291                                         sourceFC.read(bb);\r
292                                         bb.flip();  // ready for reading\r
293                                         targetFOS.getChannel().write(bb);\r
294                                 } finally {\r
295                                         sourceFIS.close();\r
296                                         targetFOS.close();\r
297                                 }\r
298                         } finally {\r
299                                 tt.done();\r
300                         }\r
301                         return load(trans.info(),targetDir,targetFileName,mediaType,0);\r
302                 }\r
303         }\r
304 \r
305         public void invalidate(String key) {\r
306                 content.remove(key);\r
307         }\r
308         \r
309         private static final Content NULL=new Content() {\r
310                 \r
311                 @Override\r
312                 public void setHeader(HttpServletResponse resp) {\r
313                         resp.setStatus(HttpStatus.NOT_FOUND_404);\r
314                         resp.setHeader("Content-type","text/plain");\r
315                 }\r
316 \r
317                 @Override\r
318                 public void write(Writer writer) throws IOException {\r
319                 }\r
320 \r
321                 @Override\r
322                 public void write(OutputStream os) throws IOException {\r
323                 }\r
324                 \r
325         };\r
326 \r
327         private static abstract class Content {\r
328                 private long date;   // date of the actual artifact (i.e. File modified date)\r
329                 private long access; // last accessed\r
330                 \r
331                 protected String  contentType;\r
332                 protected boolean attachmentOnly;\r
333                 \r
334                 public void setHeader(HttpServletResponse resp) {\r
335                         resp.setStatus(HttpStatus.OK_200);\r
336                         resp.setHeader("Content-type",contentType);\r
337                         resp.setHeader("Cache-Control", MAX_AGE);\r
338                 }\r
339                 \r
340                 public abstract void write(Writer writer) throws IOException;\r
341                 public abstract void write(OutputStream os) throws IOException;\r
342 \r
343         }\r
344 \r
345         private static class DirectFileContent extends Content {\r
346                 private File file; \r
347                 public DirectFileContent(File f) {\r
348                         file = f;\r
349                 }\r
350                 \r
351                 public String toString() {\r
352                         return file.getName();\r
353                 }\r
354                 \r
355                 public void write(Writer writer) throws IOException {\r
356                         FileReader fr = new FileReader(file);\r
357                         char[] buff = new char[1024];\r
358                         try {\r
359                                 int read;\r
360                                 while((read = fr.read(buff,0,1024))>=0) {\r
361                                         writer.write(buff,0,read);\r
362                                 }\r
363                         } finally {\r
364                                 fr.close();\r
365                         }\r
366                 }\r
367 \r
368                 public void write(OutputStream os) throws IOException {\r
369                         FileInputStream fis = new FileInputStream(file);\r
370                         byte[] buff = new byte[1024];\r
371                         try {\r
372                                 int read;\r
373                                 while((read = fis.read(buff,0,1024))>=0) {\r
374                                         os.write(buff,0,read);\r
375                                 }\r
376                         } finally {\r
377                                 fis.close();\r
378                         }\r
379                 }\r
380 \r
381         }\r
382         private static class CachedContent extends Content {\r
383                 private byte[] data;\r
384                 private int end;\r
385                 private char[] cdata; \r
386                 \r
387                 public CachedContent(File f) throws IOException {\r
388                         // Read and Cache\r
389                         ByteBuffer bb = ByteBuffer.allocate((int)f.length());\r
390                         FileInputStream fis = new FileInputStream(f);\r
391                         try {\r
392                                 fis.getChannel().read(bb);\r
393                         } finally {\r
394                                 fis.close();\r
395                         }\r
396 \r
397                         data = bb.array();\r
398                         end = bb.position();\r
399                         cdata=null;\r
400                 }\r
401                 \r
402                 public String toString() {\r
403                         return data.toString();\r
404                 }\r
405                 \r
406                 public void write(Writer writer) throws IOException {\r
407                         synchronized(this) {\r
408                                 // do the String Transformation once, and only if actually used\r
409                                 if(cdata==null) {\r
410                                         cdata = new char[end];\r
411                                         new String(data).getChars(0, end, cdata, 0);\r
412                                 }\r
413                         }\r
414                         writer.write(cdata,0,end);\r
415                 }\r
416                 public void write(OutputStream os) throws IOException {\r
417                         os.write(data,0,end);\r
418                 }\r
419 \r
420         }\r
421 \r
422         public void setEnv(LogTarget env) {\r
423                 logT = env;\r
424         }\r
425 \r
426         /**\r
427          * Cleanup thread to remove older items if max Cache is reached.\r
428          *\r
429          */\r
430         private static class Cleanup extends TimerTask {\r
431                 private int maxSize;\r
432                 private NavigableMap<String, Content> content;\r
433                 \r
434                 public Cleanup(NavigableMap<String, Content> content, int size) {\r
435                         maxSize = size;\r
436                         this.content = content;\r
437                 }\r
438                 \r
439                 private class Comp implements Comparable<Comp> {\r
440                         public Map.Entry<String, Content> entry;\r
441                         \r
442                         public Comp(Map.Entry<String, Content> en) {\r
443                                 entry = en;\r
444                         }\r
445                         \r
446                         @Override\r
447                         public int compareTo(Comp o) {\r
448                                 return (int)(entry.getValue().access-o.entry.getValue().access);\r
449                         }\r
450                         \r
451                 }\r
452                 @SuppressWarnings("unchecked")\r
453                 @Override\r
454                 public void run() {\r
455                         int size = content.size();\r
456                         if(size>maxSize) {\r
457                                 ArrayList<Comp> scont = new ArrayList<Comp>(size);\r
458                                 Object[] entries = content.entrySet().toArray();\r
459                                 for(int i=0;i<size;++i) {\r
460                                         scont.add(i, new Comp((Map.Entry<String,Content>)entries[i]));\r
461                                 }\r
462                                 Collections.sort(scont);\r
463                                 int end = size - ((maxSize/4)*3); // reduce to 3/4 of max size\r
464                                 System.out.println("------ Cleanup Cycle ------ " + new Date().toString() + " -------");\r
465                                 for(int i=0;i<end;++i) {\r
466                                         Entry<String, Content> entry = scont.get(i).entry;\r
467                                         content.remove(entry.getKey());\r
468                                         System.out.println("removed Cache Item " + entry.getKey() + "/" + new Date(entry.getValue().access).toString());\r
469                                 }\r
470                                 for(int i=end;i<size;++i) {\r
471                                         Entry<String, Content> entry = scont.get(i).entry;\r
472                                         System.out.println("remaining Cache Item " + entry.getKey() + "/" + new Date(entry.getValue().access).toString());\r
473                                 }\r
474                         }\r
475                 }\r
476         }\r
477 }\r