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