2 * ============LICENSE_START====================================================
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
11 * http://www.apache.org/licenses/LICENSE-2.0
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====================================================
22 package org.onap.aaf.auth.rserv;
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.Date;
41 import java.util.HashSet;
43 import java.util.Map.Entry;
44 import java.util.NavigableMap;
46 import java.util.Timer;
47 import java.util.TimerTask;
48 import java.util.TreeMap;
49 import java.util.concurrent.ConcurrentSkipListMap;
50 import java.util.regex.Matcher;
51 import java.util.regex.Pattern;
53 import javax.servlet.http.HttpServletRequest;
54 import javax.servlet.http.HttpServletResponse;
56 import org.onap.aaf.misc.env.Env;
57 import org.onap.aaf.misc.env.EnvJAXB;
58 import org.onap.aaf.misc.env.LogTarget;
59 import org.onap.aaf.misc.env.Store;
60 import org.onap.aaf.misc.env.TimeTaken;
61 import org.onap.aaf.misc.env.Trans;
65 * Author: Jonathan Gathman, Gathsys 2010
68 public class CachingFileAccess<TRANS extends Trans> extends HttpCode<TRANS, Void> {
69 public static void setEnv(Store store, String[] args) {
70 for(int i=0;i<args.length-1;i+=2) { // cover two parms required for each
71 if(CFA_WEB_PATH.equals(args[i])) {
72 store.put(store.staticSlot(CFA_WEB_PATH), args[i+1]);
73 } else if(CFA_CACHE_CHECK_INTERVAL.equals(args[i])) {
74 store.put(store.staticSlot(CFA_CACHE_CHECK_INTERVAL), Long.parseLong(args[i+1]));
75 } else if(CFA_MAX_SIZE.equals(args[i])) {
76 store.put(store.staticSlot(CFA_MAX_SIZE), Integer.parseInt(args[i+1]));
81 private static String MAX_AGE = "max-age=3600"; // 1 hour Caching
82 private final Map<String,String> typeMap;
83 private final NavigableMap<String,Content> content;
84 private final Set<String> attachOnly;
85 public final static String CFA_WEB_PATH = "aaf_cfa_web_path";
86 // when to re-validate from file
87 // Re validating means comparing the Timestamp on the disk, and seeing it has changed. Cache is not marked
88 // dirty unless file has changed, but it still makes File IO, which for some kinds of cached data, i.e.
89 // deployed GUI elements is unnecessary, and wastes time.
90 // This parameter exists to cover the cases where data can be more volatile, so the user can choose how often the
91 // File IO will be accessed, based on probability of change. "0", of course, means, check every time.
92 private final static String CFA_CACHE_CHECK_INTERVAL = "aaf_cfa_cache_check_interval";
93 private final static String CFA_MAX_SIZE = "aaf_cfa_max_size"; // Cache size limit
94 private final static String CFA_CLEAR_COMMAND = "aaf_cfa_clear_command";
96 // Note: can be null without a problem, but included
97 // to tie in with existing Logging.
98 public LogTarget logT = null;
99 public long checkInterval; // = 600000L; // only check if not hit in 10 mins by default
100 public int maxItemSize; // = 512000; // max file 500k
102 private String web_path;
103 // A command key is set in the Properties, preferably changed on deployment.
104 // it is compared at the beginning of the path, and if so, it is assumed to issue certain commands
105 // It's purpose is to protect, to some degree the command, even though it is HTTP, allowing
106 // local batch files to, for instance, clear caches on resetting of files.
107 private String clear_command;
109 public CachingFileAccess(EnvJAXB env, String ... args) throws IOException {
110 super(null,"Caching File Access");
112 content = new ConcurrentSkipListMap<String,Content>(); // multi-thread changes possible
114 attachOnly = new HashSet<String>(); // short, unchanged
116 typeMap = new TreeMap<String,String>(); // Structure unchanged after Construction
117 typeMap.put("ico","image/icon");
118 typeMap.put("html","text/html");
119 typeMap.put("css","text/css");
120 typeMap.put("js","text/javascript");
121 typeMap.put("txt","text/plain");
122 typeMap.put("xml","text/xml");
123 typeMap.put("xsd","text/xml");
124 attachOnly.add("xsd");
125 typeMap.put("crl", "application/x-pkcs7-crl");
126 typeMap.put("appcache","text/cache-manifest");
128 typeMap.put("json","text/json");
129 typeMap.put("ogg", "audio/ogg");
130 typeMap.put("jpg","image/jpeg");
131 typeMap.put("gif","image/gif");
132 typeMap.put("png","image/png");
133 typeMap.put("svg","image/svg+xml");
134 typeMap.put("jar","application/x-java-applet");
135 typeMap.put("jnlp", "application/x-java-jnlp-file");
136 typeMap.put("class", "application/java");
137 typeMap.put("props", "text/plain");
138 typeMap.put("jks", "application/octet-stream");
140 timer = new Timer("Caching Cleanup",true);
141 timer.schedule(new Cleanup(content,500),60000,60000);
144 web_path = env.get(env.staticSlot(CFA_WEB_PATH));
145 env.init().log("CachingFileAccess path: " + new File(web_path).getCanonicalPath());
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);}
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);}
155 clear_command = env.getProperty(CFA_CLEAR_COMMAND,null);
161 public void handle(TRANS trans, HttpServletRequest req, HttpServletResponse resp) throws IOException {
162 String key = pathParam(req, ":key");
163 String cmd = pathParam(req,":cmd");
164 System.out.print(key + clear_command);
165 if(key.equals(clear_command)) {
166 resp.setHeader("Content-Type",typeMap.get("txt"));
167 if("clear".equals(cmd)) {
169 resp.setStatus(200/*HttpStatus.OK_200*/);
171 resp.setStatus(400/*HttpStatus.BAD_REQUEST_400 */);
175 Content c = load(logT , web_path,cmd!=null && cmd.length()>0?key+'/'+cmd:key, null, checkInterval);
176 if(c.attachmentOnly) {
177 resp.setHeader("Content-disposition", "attachment");
180 c.write(resp.getOutputStream());
181 trans.checkpoint(req.getPathInfo());
185 public String webPath() {
190 * Reset the Cleanup size and interval
192 * The size and interval when started are 500 items (memory size unknown) checked every minute in a background thread.
197 public void cleanupParams(int size, long interval) {
200 timer.schedule(new Cleanup(content,size), interval, interval);
206 * Load a file, first checking cache
209 * @param logTarget - logTarget can be null (won't log)
210 * @param dataRoot - data root storage directory
211 * @param key - relative File Path
212 * @param mediaType - what kind of file is it. If null, will check via file extension
213 * @param timeCheck - "-1" will take system default - Otherwise, will compare "now" + timeCheck(Millis) before looking at File mod
215 * @throws IOException
217 public Content load(LogTarget logTarget, String dataRoot, String key, String mediaType, long _timeCheck) throws IOException {
218 long timeCheck = _timeCheck;
220 timeCheck=checkInterval; // if time < 0, then use default
224 if("-".equals(key)) {
228 fileName=dataRoot + '/' + key;
231 Content c = content.get(key);
232 long systime = System.currentTimeMillis();
235 // Don't check every hit... only after certain time value
236 if(c.date < systime + timeCheck) {
237 f = new File(fileName);
238 if(f.lastModified()>c.date) {
244 if(logTarget!=null) {
245 logTarget.log("File Read: ",key);
249 f = new File(fileName);
253 if(f.isDirectory()) {
255 c = new DirectoryContent(f,isRoot);
257 if(f.length() > maxItemSize) {
258 c = new DirectFileContent(f);
261 c = new CachedContent(f);
262 cacheMe = checkInterval>0;
265 if(mediaType==null) { // determine from file Ending
266 int idx = key.lastIndexOf('.');
267 String subkey = key.substring(++idx);
268 if((c.contentType = idx<0?null:typeMap.get(subkey))==null) {
269 // if nothing else, just set to default type...
270 c.contentType = "application/octet-stream";
272 c.attachmentOnly = attachOnly.contains(subkey);
274 c.contentType=mediaType;
275 c.attachmentOnly = false;
278 c.date = f.lastModified();
288 if(logTarget!=null)logTarget.log("Cache Read: ",key);
296 public Content loadOrDefault(Trans trans, String targetDir, String targetFileName, String sourcePath, String mediaType) throws IOException {
298 return load(trans.info(),targetDir,targetFileName,mediaType,0);
299 } catch(FileNotFoundException e) {
300 String targetPath = targetDir + '/' + targetFileName;
301 TimeTaken tt = trans.start("File doesn't exist; copy " + sourcePath + " to " + targetPath, Env.SUB);
303 FileInputStream sourceFIS = new FileInputStream(sourcePath);
304 FileChannel sourceFC = sourceFIS.getChannel();
305 File targetFile = new File(targetPath);
306 targetFile.getParentFile().mkdirs(); // ensure directory exists
307 FileOutputStream targetFOS = new FileOutputStream(targetFile);
309 ByteBuffer bb = ByteBuffer.allocate((int)sourceFC.size());
311 bb.flip(); // ready for reading
312 targetFOS.getChannel().write(bb);
320 return load(trans.info(),targetDir,targetFileName,mediaType,0);
324 public void invalidate(String key) {
328 private static final Content NULL=new Content() {
331 public void setHeader(HttpServletResponse resp) {
332 resp.setStatus(404/*NOT_FOUND_404*/);
333 resp.setHeader("Content-type","text/plain");
337 public void write(Writer writer) throws IOException {
341 public void write(OutputStream os) throws IOException {
346 private static abstract class Content {
347 private long date; // date of the actual artifact (i.e. File modified date)
348 private long access; // last accessed
350 protected String contentType;
351 protected boolean attachmentOnly;
353 public void setHeader(HttpServletResponse resp) {
354 resp.setStatus(200/*OK_200*/);
355 resp.setHeader("Content-Type",contentType);
356 resp.setHeader("Cache-Control", MAX_AGE);
359 public abstract void write(Writer writer) throws IOException;
360 public abstract void write(OutputStream os) throws IOException;
364 private static class DirectFileContent extends Content {
366 public DirectFileContent(File f) {
370 public String toString() {
371 return file.getName();
374 public void write(Writer writer) throws IOException {
375 FileReader fr = new FileReader(file);
376 char[] buff = new char[1024];
379 while((read = fr.read(buff,0,1024))>=0) {
380 writer.write(buff,0,read);
387 public void write(OutputStream os) throws IOException {
388 FileInputStream fis = new FileInputStream(file);
389 byte[] buff = new byte[1024];
392 while((read = fis.read(buff,0,1024))>=0) {
393 os.write(buff,0,read);
401 private static class DirectoryContent extends Content {
402 private static final Pattern A_NUMBER = Pattern.compile("\\d");
403 private static final String H1 = "<html><head><title>AAF Fileserver</title></head><body><h1>AAF Fileserver</h1><h2>";
404 private static final String H2 = "</h2><ul>\n";
405 private static final String F = "\n</ul></body></html>";
406 private File[] files;
408 private boolean notRoot;
410 public DirectoryContent(File directory, boolean isRoot) {
413 files = directory.listFiles();
414 Arrays.sort(files,new Comparator<File>() {
416 public int compare(File f1, File f2) {
417 // See if there are Numbers in the name
418 Matcher m1 = A_NUMBER.matcher(f1.getName());
419 Matcher m2 = A_NUMBER.matcher(f2.getName());
420 if(m1.find() && m2.find()) {
421 // if numbers, are the numbers in the same start position
425 // If same start position and the text is the same, then reverse sort
426 if(i1==i2 && f1.getName().startsWith(f2.getName().substring(0,i1))) {
427 // reverse sort files that start similarly, but have numbers in them
428 return f2.compareTo(f1);
431 return f1.compareTo(f2);
435 name = directory.getName();
436 attachmentOnly = false;
437 contentType = "text/html";
442 public void write(Writer w) throws IOException {
446 for (File f : files) {
447 w.append("<li><a href=\"");
452 w.append(f.getName());
454 w.append(f.getName());
455 w.append("</a></li>\n");
462 public void write(OutputStream os) throws IOException {
463 write(new OutputStreamWriter(os));
468 private static class CachedContent extends Content {
471 private char[] cdata;
473 public CachedContent(File f) throws IOException {
475 ByteBuffer bb = ByteBuffer.allocate((int)f.length());
476 FileInputStream fis = new FileInputStream(f);
478 fis.getChannel().read(bb);
488 public String toString() {
489 return Arrays.toString(data);
492 public void write(Writer writer) throws IOException {
494 // do the String Transformation once, and only if actually used
496 cdata = new char[end];
497 new String(data).getChars(0, end, cdata, 0);
500 writer.write(cdata,0,end);
502 public void write(OutputStream os) throws IOException {
503 os.write(data,0,end);
508 public void setEnv(LogTarget env) {
513 * Cleanup thread to remove older items if max Cache is reached.
517 private static class Cleanup extends TimerTask {
519 private NavigableMap<String, Content> content;
521 public Cleanup(NavigableMap<String, Content> content, int size) {
523 this.content = content;
526 private class Comp implements Comparable<Comp> {
527 public Map.Entry<String, Content> entry;
529 public Comp(Map.Entry<String, Content> en) {
534 public int compareTo(Comp o) {
535 return (int)(entry.getValue().access-o.entry.getValue().access);
539 @SuppressWarnings("unchecked")
542 int size = content.size();
544 ArrayList<Comp> scont = new ArrayList<Comp>(size);
545 Object[] entries = content.entrySet().toArray();
546 for(int i=0;i<size;++i) {
547 scont.add(i, new Comp((Map.Entry<String,Content>)entries[i]));
549 Collections.sort(scont);
550 int end = size - ((maxSize/4)*3); // reduce to 3/4 of max size
551 System.out.println("------ Cleanup Cycle ------ " + new Date().toString() + " -------");
552 for(int i=0;i<end;++i) {
553 Entry<String, Content> entry = scont.get(i).entry;
554 content.remove(entry.getKey());
555 System.out.println("removed Cache Item " + entry.getKey() + "/" + new Date(entry.getValue().access).toString());
557 for(int i=end;i<size;++i) {
558 Entry<String, Content> entry = scont.get(i).entry;
559 System.out.println("remaining Cache Item " + entry.getKey() + "/" + new Date(entry.getValue().access).toString());