/* * Copyright (C) 2000 - 2008 TagServlet Ltd * * This file is part of Open BlueDragon (OpenBD) CFML Server Engine. * * OpenBD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * Free Software Foundation,version 3. * * OpenBD is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with OpenBD. If not, see http://www.gnu.org/licenses/ * * Additional permission under GNU GPL version 3 section 7 * * If you modify this Program, or any covered work, by linking or combining * it with any of the JARS listed in the README.txt (or a modified version of * (that library), containing parts covered by the terms of that JAR, the * licensors of this Program grant you additional permission to convey the * resulting work. * README.txt @ http://www.openbluedragon.org/license/README.txt * * http://www.openbluedragon.org/ */ /* * Created on 21-May-2004 by Alan Williamson * * Implements the CFCACHE tag * * Due to the way we retrieve the content we do not need USERNAME, PASSWORD, HTTP, PORT attributes * of the CFCACHE tag. * */ package com.naryx.tagfusion.cfm.tag; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.FileReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Serializable; import javax.servlet.RequestDispatcher; import javax.servlet.http.HttpServletResponse; import org.apache.oro.text.regex.MalformedPatternException; import org.apache.oro.text.regex.Pattern; import org.apache.oro.text.regex.PatternMatcherInput; import org.apache.oro.text.regex.Perl5Compiler; import org.apache.oro.text.regex.Perl5Matcher; import org.apache.oro.text.regex.Perl5Substitution; import org.apache.oro.text.regex.Util; import com.nary.util.Lock; import com.naryx.tagfusion.cfm.engine.catchDataFactory; import com.naryx.tagfusion.cfm.engine.cfEngine; import com.naryx.tagfusion.cfm.engine.cfSession; import com.naryx.tagfusion.cfm.engine.cfmBadFileException; import com.naryx.tagfusion.cfm.engine.cfmRunTimeException; import com.naryx.tagfusion.servlet.jsp.cfIncludeHttpServletResponseWrapper; public class cfCACHE extends cfTag implements Serializable, cfOptionalBodyTag { static final long serialVersionUID = 1; private static final int ACTION_CACHE = 0; private static final int ACTION_FLUSH = 1; private static final int ACTION_CLIENTCACHE = 2; private static final int ACTION_SERVERCACHE = 4; private static int totalCacheHit = 0; // total server cache hit (doesn't include client cache hits) private int actionType = ACTION_CACHE; private static Lock cacheLock = new Lock(); private String endMarker = null; public static int getTotalHits(){ return totalCacheHit; } public String getEndMarker() { return endMarker; } public void setEndTag() { //--[ This is called once from the cfParseTag class. its to handle <CFMODULE/> which is to trigger double execution endMarker = ""; } public void lookAheadForEndTag(tagReader inFile) { endMarker = new tagLocator("CFCACHE", inFile).findEndMarker(); } //------------------------------------ //------------------------------------ protected void defaultParameters( String _tag ) throws cfmBadFileException { defaultAttribute( "ACTION", "cache" ); defaultAttribute( "PROTOCOL", "http://" ); defaultAttribute( "PORT", "80" ); defaultAttribute( "EXPIREURL", "" ); parseTagHeader( _tag ); //--[ Get the ACTION property out String action = getConstant( "ACTION" ).toLowerCase(); if ( action.equals("cache") || action.equals("optimal") ) actionType = ACTION_CACHE; else if ( action.equals("flush") ) actionType = ACTION_FLUSH; else if ( action.equals("clientcache") ) actionType = ACTION_CLIENTCACHE; else if ( action.equals("servercache") ) actionType = ACTION_SERVERCACHE; if ( containsAttribute( "TIMEOUT" ) ){ throw newBadFileException("Unsupported Attribute", "The TIMEOUT attribute is not supported. Use TIMESPAN instead." ); } //--[ No longer required so lets remove it removeAttribute( "ACTION" ); } public cfTagReturnType render( cfSession _Session ) throws cfmRunTimeException { if ( !containsAttribute( "DIRECTORY" ) ) { defaultAttribute( "DIRECTORY", new File( cfEngine.thisPlatform.getFileIO().getWorkingDirectory(), "cfcache" ).toString() ); } if ( actionType == ACTION_CACHE ){ //-- Check to see that this execution run isn't in response to a CFCACHE call //-- If it is, then simply ignore this execution if ( _Session.REQ.getAttribute("bdcache") != null ) return cfTagReturnType.NORMAL; //-- Determine if we are to send back the page we have in cache //-- or create a new one long browserDate = getBrowserDate( _Session.REQ.getHeader("If-Modified-Since") ); File cacheName = new File( getDirectory(_Session), generateFilename( _Session ) ); String lockName = cacheName.getAbsolutePath(); // First check the client cache if ( !isClientCachedFileExpired(_Session, cacheName, browserDate, _Session.getCurrentFile().lastModified()) ){ // It's not expired in the client cache so return not modified and abort processing _Session.setStatus( 304, "Not Modified" ); _Session.abortPageProcessing(); } synchronized( cacheLock.getLock( lockName ) ){ try{ // Now check the server cache if ( isServerCachedFileExpired(_Session, cacheName, _Session.getCurrentFile().lastModified() ) ){ // It's expired in the client and server cache so send last-modified to cache it in the // client cache and call makeCacheFile() to cache it in the server cache. _Session.setHeader( "Last-Modified", com.nary.util.Date.formatNow( "EEE, dd MMM yyyy HH:mm:ss" ) + " GMT" ); String errorMsg = makeCacheFile(_Session, cacheName); if ( errorMsg != null ) throw new cfmRunTimeException( catchDataFactory.generalException("errorCode.runtimeError","runtime.general", new String[] {"Failed to cache " + _Session.getRequestURI() + " (" + errorMsg + ")"})); }else{ //-- The server cache is good totalCacheHit++; } //-- Read the cache from file and send it to the client sendCacheFile( _Session, cacheName ); }finally{ cacheLock.removeLock( lockName ); } } //-- No point in continuing any further _Session.abortPageProcessing( true ); } else if ( actionType == ACTION_SERVERCACHE ){ //-- Check to see that this execution run isn't in response to a CFCACHE call //-- If it is, then simply ignore this execution if ( _Session.REQ.getAttribute("bdcache") != null ) return cfTagReturnType.NORMAL; File cacheName = new File( getDirectory(_Session), generateFilename( _Session ) ); String lockName = cacheName.getAbsolutePath(); synchronized( cacheLock.getLock( lockName ) ){ try{ if ( isServerCachedFileExpired(_Session, cacheName, _Session.getCurrentFile().lastModified() ) ){ String errorMsg = makeCacheFile(_Session, cacheName); if ( errorMsg != null ) throw new cfmRunTimeException( catchDataFactory.generalException("errorCode.runtimeError","runtime.general", new String[] {"Failed to cache " + _Session.getRequestURI() + " (" + errorMsg + ")"})); }else{ //-- The server cache is good totalCacheHit++; } //-- Read the cache from file and send it to the client sendCacheFile( _Session, cacheName ); }finally{ cacheLock.removeLock( lockName ); } } //-- No point in continuing any further _Session.abortPageProcessing(); } else if ( actionType == ACTION_CLIENTCACHE ){ long browserDate = getBrowserDate( _Session.REQ.getHeader("If-Modified-Since") ); File cacheName = new File( getDirectory(_Session), generateFilename( _Session ) ); if ( isClientCachedFileExpired(_Session, cacheName, browserDate, _Session.getCurrentFile().lastModified()) ){ // When only the client cache is being used, we create an empty file // in the server cache so we can detect a flush action. touchLocalFile( _Session, cacheName ); _Session.setHeader( "Last-Modified", com.nary.util.Date.formatNow( "EEE, dd MMM yyyy HH:mm:ss" ) + " GMT" ); }else{ _Session.setStatus( 304, "Not Modified" ); _Session.abortPageProcessing(); } }else if ( actionType == ACTION_FLUSH ){ expireFiles( getDirectory(_Session), getDynamic(_Session,"EXPIREURL").getString(), _Session.REQ.getServerName().toLowerCase() ); } return cfTagReturnType.NORMAL; } private static long getBrowserDate(String date){ if ( date == null || date.length() == 0 ) return Long.MAX_VALUE; date = date.substring( date.indexOf(",")+1 ).trim(); date = date.substring( 0, date.lastIndexOf(" ")); java.util.Date dd = com.nary.util.date.dateTimeTokenizer.getUKDate( date ); if ( dd != null ) return dd.getTime(); else return Long.MAX_VALUE; } private File getDirectory(cfSession _Session ) throws cfmRunTimeException { File dir = new File(getDynamic(_Session,"DIRECTORY").getString()); if ( !dir.isDirectory() ) dir.mkdirs(); return dir; } private static void touchLocalFile(cfSession _Session, File localFile ){ BufferedWriter out = null; try{ String server = _Session.REQ.getServerName().toLowerCase(); String uri = _Session.getRequestURI(); String queryStr = _Session.REQ.getQueryString(); out = new BufferedWriter( cfEngine.thisPlatform.getFileIO().getFileWriter(localFile) ); if ( queryStr != null ) out.write( "<!-- " + server + uri + "?" + queryStr + " -->\r\n" ); else out.write( "<!-- " + server + uri + " -->\r\n" ); }catch(Exception ignore){ }finally{ // Make sure the writer is closed so we'll be able to delete the file for a flush action. try{if ( out != null ) out.close();}catch(Exception ignoreClose){} } } private static String generateFilename( cfSession _Session ) { String server = _Session.REQ.getServerName().toLowerCase(); String queryStr = _Session.REQ.getQueryString(); String filename = server + _Session.getRequestURI(); if ( queryStr != null ) filename += "?" + queryStr; return "cfcache_" + com.nary.util.string.hashCode(filename) + ".htm"; } private boolean isServerCachedFileExpired(cfSession _Session, File cacheName, long _pageLastModified ) throws cfmRunTimeException{ // If the file isn't cached on the server then return true to indicate it expired. if ( !cacheName.exists() ) return true; // if the cfm page has been modified since it was last cached if ( _pageLastModified > cacheName.lastModified() ){ return true; } // If a timespan was specified and the current time is greater than the cached file's // last modified time plus the timespan then return true to indicate it expired. if ( containsAttribute("TIMESPAN") ){ double timespan = getDynamic(_Session,"TIMESPAN").getDouble(); // Convert the timespan to milliseconds long timespanMillis = (long)((double)86400000 * timespan); // Check if it expired if ( System.currentTimeMillis() > cacheName.lastModified() + timespanMillis ) return true; } return false; } private boolean isClientCachedFileExpired(cfSession _Session, File cacheName, long browserDate, long _codeLastModified ) throws cfmRunTimeException { // When only the client cache is being used, we create an empty file in the server cache so we can // detect a flush action. When both the client and server cache are being used then the file in the // server cache will actually contain the last cached results. In either case, if the file doesn't // exist then we now it's been flushed so return true to indicate it expired. if ( !cacheName.exists() ) return true; // If the file doesn't exist in the client cache then return true to indicate it expired. // NOTE: This is detected by the absence of the "If-Modified-Since" request header. if ( browserDate == Long.MAX_VALUE ) return true; if ( _codeLastModified > cacheName.lastModified() ){ return true; } // If a timespan was specified and the current time is greater than the browserDate plus // the timespan then return true to indicate it expired. if ( containsAttribute("TIMESPAN") ){ double timespan = getDynamic(_Session,"TIMESPAN").getDouble(); // Convert the timespan to milliseconds long timespanMillis = (long)((double)86400000 * timespan); // Check if it expired if ( System.currentTimeMillis() > browserDate + timespanMillis ) return true; } return false; } private static String makeCacheFile( cfSession _session, File cacheName ){ String uri = _session.getRequestURI(); RequestDispatcher rd = _session.REQ.getRequestDispatcher( uri ); if ( rd == null ) { return "Failed to get RequestDispatcher"; } //---[ Now that the servlet has been found, trigger its execution cfIncludeHttpServletResponseWrapper servletOutput = new cfIncludeHttpServletResponseWrapper( _session.RES ); _session.REQ.setAttribute("bdcache", ""); try { rd.include( _session.REQ, servletOutput ); } catch ( Exception exc ) { //-- This page had an error with it; therefore, we don't cache the page, but continue to //-- execute it as a whole so the user can see the *real* error return "Requested page threw an exception - " + exc.toString(); } //-- Lets make sure all is well if ( servletOutput.getStatusCode() == HttpServletResponse.SC_TEMPORARY_REDIRECT ){ //- The page we were attempting to cache issued a redirect via CFLOCATION. //- We'll honour it here so the page isn't run twice which may cause problems try{ _session.sendRedirect( servletOutput.getRedirectURI() ); }catch(Exception ignore){} return "Failed to redirect"; } else if ( servletOutput.getStatusCode() != HttpServletResponse.SC_OK ){ return "Received status code - " + servletOutput.getStatusCode(); } //-- Write output to File BufferedWriter out = null; OutputStream fout = null; OutputStreamWriter osw = null; try{ String server = _session.REQ.getServerName().toLowerCase(); String queryStr = _session.REQ.getQueryString(); fout = cfEngine.thisPlatform.getFileIO().getFileOutputStream(cacheName); osw = new OutputStreamWriter( fout, "utf-8" ); out = new BufferedWriter( osw ); if ( queryStr != null ) out.write( "<!-- " + server + uri + "?" + queryStr + " -->\r\n" ); else out.write( "<!-- " + server + uri + " -->\r\n" ); String enc = com.nary.util.Localization.convertCharSetToCharEncoding( servletOutput.getCharacterEncoding() ); String s = new String( servletOutput.getByteArray(), enc ); out.write( s ); out.flush(); }catch(Exception E){ //-- Something went wrong with the cache output; flag this as an invalid cache creation return "Failed to write file to cache - " + E.toString(); }finally{ // Make sure the writer is closed so we'll be able to delete the file for a flush action. try{if ( out != null ) out.close();}catch(Exception ignoreClose){} } return null; } private static void sendCacheFile( cfSession _Session, File cacheName ) throws cfmRunTimeException { try{ sendCacheFile( _Session, cacheName, false ); }catch( IOException e ){ String errorMsg = makeCacheFile(_Session, cacheName); if ( errorMsg != null ) throw new cfmRunTimeException( catchDataFactory.generalException("errorCode.runtimeError","runtime.general", new String[] {"Failed to cache " + _Session.getRequestURI() + " (" + errorMsg + ")"})); try { sendCacheFile( _Session, cacheName, true ); } catch ( IOException e1 ) { // shouldn't happen but log it in any case cfEngine.log( "Unexpected exception. IOException should be masked" ); } } } private static void sendCacheFile( cfSession _Session, File cacheName, boolean _maskIOException ) throws IOException, cfmRunTimeException { BufferedReader in = null; FileInputStream fis = null; InputStreamReader isr = null; try{ fis = new FileInputStream( cacheName ); isr = new InputStreamReader( fis, com.nary.util.Localization.convertCharSetToCharEncoding( "utf-8" ) ); in = new BufferedReader( isr ); String lineIn = in.readLine(); //-- Read the first line; its BlueDragon related while ( (lineIn=in.readLine()) != null ){ _Session.write( lineIn ); _Session.write( "\r\n" ); } }catch(Exception E){ if ( ! _maskIOException && E instanceof IOException ){ throw (IOException) E; } throw new cfmRunTimeException( catchDataFactory.extendedException( "errorCode.runtimeError", "cfcache.fromdisk", new String[]{cacheName.toString()}, E.getMessage()) ); }finally{ // Make sure the reader is closed so we'll be able to delete the file for a flush action. try{if ( in != null ) in.close();}catch(Exception ignoreClose){} } } private static class cfCACHEFileFilter implements FileFilter { public boolean accept(File pathname) { if(pathname.getName().matches("cfcache_-?[0-9]+\\.htm")) return true; else return false; } } private static cfCACHEFileFilter cfCacheFileFilter = new cfCACHEFileFilter(); private void expireFiles( File directory, String expireURL, String virtualServer ) throws cfmRunTimeException { File[] listOfFiles = directory.listFiles(cfCacheFileFilter); boolean deleteAll = (expireURL.equals("*") || expireURL.length() == 0); //use of this var is the fix for NA bug #3308 boolean ignoreHost = (! deleteAll && expireURL.startsWith("*")); File thisFile; String firstline; Perl5Compiler compiler = new Perl5Compiler(); Perl5Matcher matcher = new Perl5Matcher(); Pattern pattern = null; if ( !deleteAll ){ try { if(!ignoreHost) { /* The string in the cache file is always <servername>/<uri>; we need to add in the servername and adjust the expireURL accordingly */ if ( expireURL.startsWith("/") ) expireURL = virtualServer + expireURL; else expireURL = virtualServer + "/" + expireURL; } pattern = compiler.compile( escapeExpireUrl( expireURL ) ); } catch (MalformedPatternException e) { throw new cfmRunTimeException( catchDataFactory.extendedException( "errorCode.runtimeError", "cfcache.expireUrl", new String[]{expireURL}, e.getMessage()) ); } } for ( int x=0; x < listOfFiles.length; x++ ){ thisFile = listOfFiles[x]; if ( deleteAll ) { boolean success = false; int tries = 0; for ( ; (tries < 10) && (!success); tries++ ) { if ( deleteCachedFile( thisFile ) ) success = true; } if ( !success ) { throw newRunTimeException( "Failed to delete cache file: " + thisFile ); } } else{ firstline = getURIFromFile( thisFile ); if ( firstline != null ){ if( pattern != null && matcher.contains( new PatternMatcherInput( firstline ), pattern ) ) deleteCachedFile( thisFile ); } } } } private boolean deleteCachedFile( File _f ){ synchronized( cacheLock.getLock( _f.getAbsolutePath() ) ){ try{ return _f.delete(); }finally{ cacheLock.removeLock( _f.getAbsolutePath() ); } } } private static String escapeExpireUrl(String expireURL){ Perl5Compiler compiler = new Perl5Compiler(); Perl5Matcher matcher = new Perl5Matcher(); Pattern pattern = null; try{ pattern = compiler.compile( "([+?.])" ); expireURL = Util.substitute(matcher, pattern, new Perl5Substitution( "\\\\$1" ), expireURL, Util.SUBSTITUTE_ALL ); return com.nary.util.string.replaceString(expireURL,"*",".*"); }catch(Exception E){ return null; } } private static String getURIFromFile(File file){ try{ BufferedReader in = new BufferedReader( new FileReader(file) ); String lineIn = in.readLine(); in.close(); int c1 = lineIn.indexOf("<!--"); if ( c1 == -1 ) return null; return lineIn.substring( c1+4, lineIn.indexOf("-->")-1 ); }catch(Exception E){ return null; } } }