001/**
002 *
003 * Copyright (c) 2014, the Railo Company Ltd. All rights reserved.
004 *
005 * This library is free software; you can redistribute it and/or
006 * modify it under the terms of the GNU Lesser General Public
007 * License as published by the Free Software Foundation; either 
008 * version 2.1 of the License, or (at your option) any later version.
009 * 
010 * This library is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013 * Lesser General Public License for more details.
014 * 
015 * You should have received a copy of the GNU Lesser General Public 
016 * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
017 * 
018 **/
019package lucee.runtime.tag;
020
021
022import java.io.IOException;
023import java.io.OutputStream;
024
025import javax.servlet.http.HttpServletResponse;
026
027import lucee.commons.io.IOUtil;
028import lucee.commons.io.cache.CacheEntry;
029import lucee.commons.io.res.Resource;
030import lucee.commons.io.res.util.ResourceUtil;
031import lucee.commons.lang.StringUtil;
032import lucee.runtime.PageContextImpl;
033import lucee.runtime.cache.legacy.CacheItem;
034import lucee.runtime.config.ConfigImpl;
035import lucee.runtime.exp.Abort;
036import lucee.runtime.exp.ApplicationException;
037import lucee.runtime.exp.DeprecatedException;
038import lucee.runtime.exp.ExpressionException;
039import lucee.runtime.exp.PageException;
040import lucee.runtime.exp.TemplateException;
041import lucee.runtime.ext.tag.BodyTagImpl;
042import lucee.runtime.functions.cache.CacheGet;
043import lucee.runtime.functions.cache.CachePut;
044import lucee.runtime.functions.cache.CacheRemove;
045import lucee.runtime.functions.cache.Util;
046import lucee.runtime.functions.dateTime.GetHttpTimeString;
047import lucee.runtime.net.http.ReqRspUtil;
048import lucee.runtime.op.Caster;
049import lucee.runtime.tag.util.DeprecatedUtil;
050import lucee.runtime.type.StructImpl;
051import lucee.runtime.type.dt.DateTime;
052import lucee.runtime.type.dt.DateTimeImpl;
053import lucee.runtime.type.dt.TimeSpan;
054import lucee.runtime.type.dt.TimeSpanImpl;
055
056import org.apache.oro.text.regex.MalformedPatternException;
057
058/**
059* Speeds up page rendering when dynamic content does not have to be retrieved each time a user accesses
060*   the page. To accomplish this, cfcache creates temporary files that contain the static HTML returned from
061*   a CFML page. You can use cfcache for simple URLs and URLs that contain URL parameters.
062*
063*
064*
065**/
066public final class Cache extends BodyTagImpl {
067
068
069
070
071        private static final TimeSpan TIMESPAN_FAR_AWAY = new TimeSpanImpl(1000000000,1000000000,1000000000,1000000000); 
072        private static final TimeSpan TIMESPAN_0 = new TimeSpanImpl(0,0,0,0); 
073
074
075        /**  */
076        private Resource directory;
077
078
079        /** Specifies the protocol used to create pages from cache. Either http:// or https://. The default
080        **              is http://. */
081        private String protocol;
082
083        /**  */
084        private String expireurl;
085
086        /**  */
087        private int action=CACHE;
088
089        /** When required for basic authentication, a valid username. */
090        //private String username;
091
092        /** When required for basic authentication, a valid password. */
093        //private String password;
094    
095        private TimeSpan timespan=TIMESPAN_FAR_AWAY;
096        private TimeSpan idletime=TIMESPAN_0;
097    
098
099        /**  */
100        private int port=-1;
101
102        private DateTimeImpl now;
103
104
105        private String body;
106        private String _id;
107        private Object id;
108        private String name;
109        private String key;
110
111
112        private boolean hasBody;
113
114
115        private boolean doCaching;
116
117
118        private CacheItem cacheItem;
119
120
121        private String cachename;
122        private Object value;
123        private boolean throwOnError;
124        private String metadata;
125    
126    private static final int CACHE=0;
127    private static final int CACHE_SERVER=1;
128    private static final int CACHE_CLIENT=2;
129    private static final int FLUSH=3;
130    private static final int CONTENT=4;
131
132    private static final int GET=5;
133    private static final int PUT=6;
134    
135    @Override
136    public void release()   {
137        super.release();
138        directory=null;
139        //username=null;
140        //password=null;
141        protocol=null;
142        expireurl=null;
143        action=CACHE;
144        port=-1;
145        timespan=TIMESPAN_FAR_AWAY;
146        idletime=TIMESPAN_0;
147        body=null;
148        hasBody=false;
149        id=null;
150        key=null;
151        body=null;
152        doCaching=false;
153        cacheItem=null;
154        name=null;
155        cachename=null;
156        throwOnError=false;
157        value=null;
158        metadata=null;
159    }
160        
161        /**
162         * @deprecated this attribute is deprecated and will ignored in this tag
163         * @param obj
164         * @throws DeprecatedException
165         */
166        public void setTimeout(Object obj) throws DeprecatedException {
167                DeprecatedUtil.tagAttribute(pageContext,"Cache","timeout");
168        }
169
170        /** set the value directory
171        *  
172        * @param directory value to set
173        **/
174        public void setDirectory(String directory) throws ExpressionException   {
175                this.directory=ResourceUtil.toResourceExistingParent(pageContext, directory);
176        }
177        public void setCachedirectory(String directory) throws ExpressionException      {
178                setDirectory(directory);
179        }
180        
181
182        /** set the value protocol
183        *  Specifies the protocol used to create pages from cache. Either http:// or https://. The default
184        *               is http://.
185        * @param protocol value to set
186        **/
187        public void setProtocol(String protocol)        {
188                if(protocol.endsWith("://"))protocol=protocol.substring(0,protocol.indexOf("://"));
189                this.protocol=protocol.toLowerCase();
190        }
191        
192        /*private String getProtocol()  {
193                if(StringUtil.isEmpty(protocol)) {
194                        return pageContext. getHttpServletRequest().getScheme();
195                }
196                return protocol;
197        }*/
198
199        /** set the value expireurl
200        *  
201        * @param expireurl value to set
202        **/
203        public void setExpireurl(String expireurl)      {
204                this.expireurl=expireurl;
205        }
206
207        /** set the value action
208        *  
209        * @param action value to set
210         * @throws ApplicationException 
211        **/
212        public void setAction(String action) throws ApplicationException        {
213        action=action.toLowerCase().trim();
214        if(action.equals("get"))              this.action=GET; 
215        else if(action.equals("put"))              this.action=PUT; 
216        
217        
218        else if(action.equals("cache"))         this.action=CACHE; 
219        else if(action.equals("clientcache"))   this.action=CACHE_CLIENT;
220        else if(action.equals("servercache"))   this.action=CACHE_SERVER;
221        else if(action.equals("flush"))         this.action=FLUSH;
222        else if(action.equals("optimal"))       this.action=CACHE;
223        
224        else if(action.equals("client-cache"))   this.action=CACHE_CLIENT;
225        else if(action.equals("client_cache"))   this.action=CACHE_CLIENT;
226        
227        else if(action.equals("server-cache"))   this.action=CACHE_SERVER;
228        else if(action.equals("server_cache"))   this.action=CACHE_SERVER;
229        
230        else if(action.equals("content"))       this.action=CONTENT;
231        else if(action.equals("content_cache"))  this.action=CONTENT;
232        else if(action.equals("contentcache"))  this.action=CONTENT;
233        else if(action.equals("content-cache"))  this.action=CONTENT;
234        else throw new ApplicationException("invalid value for attribute action for tag cache ["+action+"], " +
235                        "valid actions are [get,put,cache, clientcache, servercache, flush, optimal, contentcache]");
236        
237        //get: get an object from the cache.
238        //put: Add an object to the cache.
239        
240
241        
242        
243        }
244
245        /** set the value username
246        *  When required for basic authentication, a valid username.
247        * @param username value to set
248        **/
249        public void setUsername(String username)        {
250                //this.username=username;
251        }
252        
253        /** set the value password
254        *  When required for basic authentication, a valid password.
255        * @param password value to set
256        **/
257        public void setPassword(String password)        {
258                //this.password=password;
259        }
260        
261        public void setKey(String key)  {
262                this.key=key;
263        }
264
265        /** set the value port
266        *  
267        * @param port value to set
268        **/
269        public void setPort(double port)        {
270                this.port=(int)port;
271        }
272        
273        public int getPort() {
274                if(port<=0) return pageContext. getHttpServletRequest().getServerPort();
275                return port;
276        }
277
278    /**
279     * @param timespan The timespan to set.
280     * @throws PageException 
281     */
282    public void setTimespan(TimeSpan timespan) {
283        this.timespan = timespan;
284    }
285    
286    
287    @Override
288        public int doStartTag() throws PageException    {
289                now = new DateTimeImpl(pageContext.getConfig());
290                if(action==CACHE && hasBody) action=CONTENT;
291                try {
292                if(action==CACHE) {
293                    doClientCache();
294                    doServerCache();
295                }
296                else if(action==CACHE_CLIENT)   doClientCache();
297                else if(action==CACHE_SERVER)   doServerCache();
298                else if(action==FLUSH)                  doFlush();
299                else if(action==CONTENT) return doContentCache();
300                else if(action==GET) doGet();
301                else if(action==PUT) doPut();
302                
303                        return EVAL_PAGE;
304                }
305                catch(Exception e) {
306                        throw Caster.toPageException(e);
307                }
308        }
309
310        @Override
311        public void doInitBody()        {
312                
313        }
314
315        @Override
316        public int doAfterBody()        {
317                //print.out("doAfterBody");
318                if(bodyContent!=null)body=bodyContent.getString();
319                return SKIP_BODY;
320        }
321
322    @Override
323        public int doEndTag() throws PageException      {//print.out("doEndTag"+doCaching+"-"+body);
324                if(doCaching && body!=null) {
325                try {
326                                writeCacheResource(cacheItem, body);
327                                pageContext.write(body);
328                        }
329                catch (IOException e) {
330                                throw Caster.toPageException(e);
331                        }
332                }
333                return EVAL_PAGE;
334        }
335
336        private void doClientCache() {
337        pageContext.setHeader("Last-Modified",GetHttpTimeString.call(pageContext,now));
338        
339        if(timespan!=null) {
340            DateTime expires = getExpiresDate();
341            pageContext.setHeader("Expires",GetHttpTimeString.call(pageContext,expires));
342        }
343    }
344
345        private void doServerCache() throws IOException, PageException {
346                if(hasBody)hasBody=!StringUtil.isEmpty(body);
347                
348        // call via cfcache disable debugger output
349            if(pageContext.getConfig().debug())pageContext.getDebugger().setOutput(false);
350                
351        HttpServletResponse rsp = pageContext.getHttpServletResponse();
352        
353        // generate cache resource matching request object
354        CacheItem ci = generateCacheResource(null,false);
355        
356        // use cached resource
357        if(ci.isValid(timespan)){ //if(isOK(cacheResource)){
358                if(pageContext. getHttpServletResponse().isCommitted()) return;
359                
360                OutputStream os=null;
361                try {
362                ci.writeTo(os=getOutputStream(),ReqRspUtil.getCharacterEncoding(pageContext,rsp).name());
363                        //IOUtil.copy(is=cacheResource.getInputStream(),os=getOutputStream(),false,false);
364            } 
365            finally {
366                IOUtil.flushEL(os);
367                IOUtil.closeEL(os);
368                ((PageContextImpl)pageContext).getRootOut().setClosed(true);
369            }
370                throw new Abort(Abort.SCOPE_REQUEST);
371        }
372        
373        // call page again and
374        //MetaData.getInstance(getDirectory()).add(ci.getName(), ci.getRaw());
375        
376        PageContextImpl pci = (PageContextImpl)pageContext;
377        pci.getRootOut().doCache(ci);
378                
379    }
380
381        /*private boolean isOK(Resource cacheResource) {
382                return cacheResource.exists() && (cacheResource.lastModified()+timespan.getMillis()>=System.currentTimeMillis());
383        }*/
384
385        private int doContentCache() throws IOException {
386
387        // file
388        cacheItem = generateCacheResource(key,true);
389        // use cache
390        if(cacheItem.isValid(timespan)){
391                pageContext.write(cacheItem.getValue());
392                doCaching=false;
393            return SKIP_BODY;
394        }
395        doCaching=true;
396        return EVAL_BODY_BUFFERED;
397        }
398
399        private void doGet() throws PageException, IOException {
400                required("cache", "id", id);
401                required("cache", "name", name);
402                String id=Caster.toString(this.id);
403                if(metadata==null){
404                        pageContext.setVariable(name,CacheGet.call(pageContext, id,throwOnError,cachename));    
405                }
406                else {
407                        lucee.commons.io.cache.Cache cache = 
408                                Util.getCache(pageContext,cachename,ConfigImpl.CACHE_DEFAULT_OBJECT);
409                        CacheEntry entry = throwOnError?cache.getCacheEntry(Util.key(id)):cache.getCacheEntry(Util.key(id),null);
410                        if(entry!=null){
411                                pageContext.setVariable(name,entry.getValue());
412                                pageContext.setVariable(metadata,entry.getCustomInfo());
413                        }
414                        else {
415                                pageContext.setVariable(metadata,new StructImpl());
416                        }
417                }
418                
419                
420                
421                
422                
423                
424                
425                
426        }
427        private void doPut() throws PageException {
428                required("cache", "id", id);
429                required("cache", "value", value);
430                TimeSpan ts = timespan;
431                TimeSpan it = idletime;
432                if(ts==TIMESPAN_FAR_AWAY)ts=TIMESPAN_0;
433                if(it==TIMESPAN_FAR_AWAY)it=TIMESPAN_0; 
434                CachePut.call(pageContext, Caster.toString(id),value,ts,it,cachename);
435        }
436        
437
438        
439
440        private void doFlush() throws IOException, MalformedPatternException, PageException {
441                if(id!=null){
442                        required("cache", "id", id);
443                        CacheRemove.call(pageContext, id,throwOnError,cachename);
444                }
445                else if(StringUtil.isEmpty(expireurl)) {
446                        CacheItem.flushAll(pageContext,directory,cachename);
447                }
448                else {
449                        CacheItem.flush(pageContext,directory,cachename,expireurl);
450                        
451                        //ResourceUtil.removeChildrenEL(getDirectory(),(ResourceNameFilter)new ExpireURLFilter(expireurl));
452                }
453    }
454
455
456    private CacheItem generateCacheResource(String key, boolean useId) throws IOException {
457        return CacheItem.getInstance(pageContext,_id,key,useId,directory,cachename,timespan);
458        }
459
460        
461
462        private void writeCacheResource(CacheItem cacheItem, String result) throws IOException {
463                cacheItem.store(result);
464                //IOUtil.write(cacheItem.getResource(), result,"UTF-8", false); 
465                //MetaData.getInstance(cacheItem.getDirectory()).add(cacheItem.getName(), cacheItem.getRaw());
466        }
467        
468
469    private DateTime getExpiresDate() {
470                return new DateTimeImpl(pageContext,getExpiresTime(),false);
471        }
472
473        private long getExpiresTime() {
474                return now.getTime()+(timespan.getMillis());
475        }
476        
477        private OutputStream getOutputStream() throws PageException, IOException {
478        try {
479                return ((PageContextImpl)pageContext).getResponseStream();
480        } 
481        catch(IllegalStateException ise) {
482            throw new TemplateException("content is already send to user, flush");
483        }
484    }
485        
486        
487        /**
488     * sets if tag has a body or not
489     * @param hasBody
490     */
491    public void hasBody(boolean hasBody) {
492       this.hasBody=hasBody;
493    }
494
495        /**
496         * @param id the id to set
497         */
498        public void set_id(String _id) {
499                this._id = _id;
500        }
501        
502        public void setId(Object id) {
503                this.id = id;
504        }
505        
506        public void setName(String name) {
507                this.name = name;
508        }
509        public void setCachename(String cachename) {
510                this.cachename = cachename;
511        }
512        
513        /**
514         * @param throwOnError the throwOnError to set
515         */
516        public void setThrowonerror(boolean throwOnError) {
517                this.throwOnError = throwOnError;
518        }
519        
520        public void setValue(Object value) {
521                this.value = value;
522        }
523        
524        /**
525         * @param idletime the idletime to set
526         */
527        public void setIdletime(TimeSpan idletime) {
528                this.idletime = idletime;
529        }
530
531        /**
532         * @param metadata the metadata to set
533         */
534        public void setMetadata(String metadata) {
535                this.metadata = metadata;
536        }
537}
538