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