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