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