1 /*******************************************************************************
\r
2 * ============LICENSE_START====================================================
\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
11 * * http://www.apache.org/licenses/LICENSE-2.0
\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
20 * * ECOMP is a trademark and service mark of AT&T Intellectual Property.
\r
22 ******************************************************************************/
\r
23 package org.onap.aaf.cssa.rserv;
\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
49 import javax.servlet.http.HttpServletRequest;
\r
50 import javax.servlet.http.HttpServletResponse;
\r
52 import com.att.aft.dme2.internal.jetty.http.HttpStatus;
\r
53 import org.onap.aaf.inno.env.Env;
\r
54 import org.onap.aaf.inno.env.EnvJAXB;
\r
55 import org.onap.aaf.inno.env.LogTarget;
\r
56 import org.onap.aaf.inno.env.Store;
\r
57 import org.onap.aaf.inno.env.TimeTaken;
\r
58 import org.onap.aaf.inno.env.Trans;
\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
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
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
106 public CachingFileAccess(EnvJAXB env, String ... args) {
\r
107 super(null,"Caching File Access");
\r
109 content = new ConcurrentSkipListMap<String,Content>(); // multi-thread changes possible
\r
111 attachOnly = new HashSet<String>(); // short, unchanged
\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
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
135 timer = new Timer("Caching Cleanup",true);
\r
136 timer.schedule(new Cleanup(content,500),60000,60000);
\r
139 web_path = env.getProperty(CFA_WEB_DIR,WEB_DIR_DEFAULT);
\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
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
149 clear_command = env.getProperty(CFA_CLEAR_COMMAND,null);
\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
162 resp.setStatus(HttpStatus.OK_200);
\r
164 resp.setStatus(HttpStatus.BAD_REQUEST_400);
\r
168 Content c = load(logT , web_path,key, null, checkInterval);
\r
169 if(c.attachmentOnly) {
\r
170 resp.setHeader("Content-disposition", "attachment");
\r
172 c.write(resp.getOutputStream());
\r
174 trans.checkpoint(req.getPathInfo());
\r
178 public String webPath() {
\r
183 * Reset the Cleanup size and interval
\r
185 * The size and interval when started are 500 items (memory size unknown) checked every minute in a background thread.
\r
190 public void cleanupParams(int size, long interval) {
\r
192 timer.schedule(new Cleanup(content,size), interval, interval);
\r
198 * Load a file, first checking cache
\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
207 * @throws IOException
\r
209 public Content load(LogTarget logTarget, String dataRoot, String key, String mediaType, long _timeCheck) throws IOException {
\r
210 long timeCheck = _timeCheck;
\r
212 timeCheck=checkInterval; // if time < 0, then use default
\r
214 String fileName = dataRoot + '/' + key;
\r
215 Content c = content.get(key);
\r
216 long systime = System.currentTimeMillis();
\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
228 if(logTarget!=null) {
\r
229 logTarget.log("File Read: ",key);
\r
233 f = new File(fileName);
\r
238 if(f.length() > maxItemSize) {
\r
239 c = new DirectFileContent(f);
\r
242 c = new CachedContent(f);
\r
243 cacheMe = checkInterval>0;
\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
253 c.attachmentOnly = attachOnly.contains(subkey);
\r
255 c.contentType=mediaType;
\r
256 c.attachmentOnly = false;
\r
259 c.date = f.lastModified();
\r
262 content.put(key, c);
\r
268 if(logTarget!=null)logTarget.log("Cache Read: ",key);
\r
271 // refresh hit time
\r
272 c.access = systime;
\r
276 public Content loadOrDefault(Trans trans, String targetDir, String targetFileName, String sourcePath, String mediaType) throws IOException {
\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
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
289 ByteBuffer bb = ByteBuffer.allocate((int)sourceFC.size());
\r
291 bb.flip(); // ready for reading
\r
292 targetFOS.getChannel().write(bb);
\r
300 return load(trans.info(),targetDir,targetFileName,mediaType,0);
\r
304 public void invalidate(String key) {
\r
305 content.remove(key);
\r
308 private static final Content NULL=new Content() {
\r
311 public void setHeader(HttpServletResponse resp) {
\r
312 resp.setStatus(HttpStatus.NOT_FOUND_404);
\r
313 resp.setHeader("Content-type","text/plain");
\r
317 public void write(Writer writer) throws IOException {
\r
321 public void write(OutputStream os) throws IOException {
\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
330 protected String contentType;
\r
331 protected boolean attachmentOnly;
\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
339 public abstract void write(Writer writer) throws IOException;
\r
340 public abstract void write(OutputStream os) throws IOException;
\r
344 private static class DirectFileContent extends Content {
\r
345 private File file;
\r
346 public DirectFileContent(File f) {
\r
350 public String toString() {
\r
351 return file.getName();
\r
354 public void write(Writer writer) throws IOException {
\r
355 FileReader fr = new FileReader(file);
\r
356 char[] buff = new char[1024];
\r
359 while((read = fr.read(buff,0,1024))>=0) {
\r
360 writer.write(buff,0,read);
\r
367 public void write(OutputStream os) throws IOException {
\r
368 FileInputStream fis = new FileInputStream(file);
\r
369 byte[] buff = new byte[1024];
\r
372 while((read = fis.read(buff,0,1024))>=0) {
\r
373 os.write(buff,0,read);
\r
381 private static class CachedContent extends Content {
\r
382 private byte[] data;
\r
384 private char[] cdata;
\r
386 public CachedContent(File f) throws IOException {
\r
388 ByteBuffer bb = ByteBuffer.allocate((int)f.length());
\r
389 FileInputStream fis = new FileInputStream(f);
\r
391 fis.getChannel().read(bb);
\r
397 end = bb.position();
\r
401 public String toString() {
\r
402 return data.toString();
\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
409 cdata = new char[end];
\r
410 new String(data).getChars(0, end, cdata, 0);
\r
413 writer.write(cdata,0,end);
\r
415 public void write(OutputStream os) throws IOException {
\r
416 os.write(data,0,end);
\r
421 public void setEnv(LogTarget env) {
\r
426 * Cleanup thread to remove older items if max Cache is reached.
\r
429 private static class Cleanup extends TimerTask {
\r
430 private int maxSize;
\r
431 private NavigableMap<String, Content> content;
\r
433 public Cleanup(NavigableMap<String, Content> content, int size) {
\r
435 this.content = content;
\r
438 private class Comp implements Comparable<Comp> {
\r
439 public Map.Entry<String, Content> entry;
\r
441 public Comp(Map.Entry<String, Content> en) {
\r
446 public int compareTo(Comp o) {
\r
447 return (int)(entry.getValue().access-o.entry.getValue().access);
\r
451 @SuppressWarnings("unchecked")
\r
453 public void run() {
\r
454 int size = content.size();
\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
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
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