Permalink
Browse files

Added response caching hooks plus sample application-scoped cache imp…

…lementation.
  • Loading branch information...
timblair committed Jul 24, 2009
1 parent 522f37c commit fb56056650083fe6b473093a60e9a9d32c964e01
@@ -361,6 +361,36 @@ This component is the internal representation of a request; it's generally not u
This is simply a collection of all `Route`s available to the REST implementation; it uses the `findRoute` function to match the HTTP request method and URI to a single route.
### Response Caching
A simple response caching system is available within RESTfulCF which caches the response object for a given request (based on the request type and URI). Multiple requests for the same (cached) data will bypass the controller and simply return the relevant `Response`.
> Caching is only applicable to `GET` requests. If the cache status of the response to a non-`GET` request will be ignored and will never be cached (or asked to be retrieved from the cache).
As an example, to use the built-in `application`-scoped cache, add the following line to your `Dispatcher#init`:
<cfset setCache(createobject("component", "restfulcf.framework.core.cache.ApplicationCache").init())>
Then, in the relevant `Controller` function, set that the response can be cached as follows:
<cfset arguments['_response'].setCacheStatus(TRUE)>
The default cache time for this cache is 30 minutes; to change this you can pass a timestamp through to the `setCacheStatus()` function. For example, to set the responses to a given action to cache for an hour you'd add the following to the controller function:
<cfset arguments['_response'].setCacheStatus(createtimestamp(0,1,0,0))>
> The `ApplicationCache` is not recommended for production use: use it as an example of what you need to do to create your own concrete cache (using memcached or something similar), as described below.
#### Custom Caches
You may create you own cache type by creating a new component that extends `restfulcf.framework.core.cache.AbstractCache` and implementing the following functions:
* `getKey`
* `setKey`
* `deleteKey`
Look at the code for `restfulcf.framework.core.cache.ApplicationCache` for the arguments to these functions etc.
### HTTP Response Status Codes Used
The HTTP response code will be one of the following:
@@ -6,10 +6,6 @@ In no particular order...
* Regex or similar support for route parameters, e.g. /resources/[0-9]+
* Use of Accept: header for defining the response type, in addition to file extension
* Caching layer
* Needs to be extracted from existing WLD implementation
* `Application`-scoped cache, plus example of a simple memcached cache
* Unit tests required
* Error handling
* Handling of CF errors
* Needs to be extracted from existing WLD implementation and refined
@@ -15,6 +15,9 @@
<cfset variables.controller_path = "">
<!--- default authenticator is empty --->
<cfset variables.authenticator = createobject("component", "restfulcf.framework.core.Authenticator")>
<!--- as is the default cache --->
<cfset variables.cache_enabled = FALSE>
<cfset variables.cache = createobject("component", "restfulcf.framework.core.cache.EmptyCache")>
<!--- the default response type, should nothing be specified --->
<cfset variables.response_type = "xml">
<!--- response type / MIME type mappings --->
@@ -164,6 +167,22 @@
<cfset variables.authenticator = arguments.authenticator>
</cffunction>
<cffunction name="setCache" access="private" returntype="void" output="no" hint="Sets the cache component to use when caching responses. Automatically sets cache_enabled flag to TRUE.">
<cfargument name="cache" type="restfulcf.framework.core.cache.AbstractCache" required="yes" hint="The cache isntance">
<cfset variables.cache = arguments.cache>
<cfset variables.cache_enabled = TRUE>
</cffunction>
<cffunction name="getCache" access="public" returntype="restfulcf.framework.core.cache.AbstractCache" output="no" hint="Gets the cache component to use when caching responses">
<cfreturn variables.cache>
</cffunction>
<cffunction name="setCacheEnabled" access="public" returntype="boolean" output="no" hint="Sets if response caching is enabled">
<cfargument name="enabled" type="boolean" required="no" default="TRUE" hint="Should the cache be enabled?">
<cfset variables.cache_enabled = NOT NOT arguments.enabled>
</cffunction>
<cffunction name="isCacheEnabled" access="public" returntype="boolean" output="no" hint="Is response caching enabled?">
<cfreturn variables.cache_enabled>
</cffunction>
<cffunction name="getController" access="public" returntype="restfulcf.framework.core.Controller" output="no" hint="Returns the controller instance for a given name">
<cfargument name="name" type="string" required="yes" hint="The controller name to return">
<cfif structkeyexists(variables.controllers, arguments.name)>
@@ -48,6 +48,13 @@
<cfset var vo = "">
<cfset var resp = "">
<cfset var args = duplicate(variables.instance.arguments)>
<cfset var cache_data = {}>
<!--- check for any previously cached results (GET requests only) --->
<cfif variables.instance.dispatcher.isCacheEnabled() AND variables.instance.route.getVerb() EQ "GET">
<cfset cache_data = variables.instance.dispatcher.getCache().get(this)>
<cfif cache_data.found><cfreturn cache_data.response></cfif>
</cfif>
<!--- add the request and response objects in whatever --->
<cfset args['_request'] = this>
@@ -90,6 +97,14 @@
</cfif>
</cfif>
<!--- cache the response if required (we only care about successful GETs) --->
<cfif variables.instance.dispatcher.isCacheEnabled()>
<cfif variables.instance.route.getVerb() EQ "GET" AND response.getCacheStatus()
AND response.getStatusCode() EQ controller.HTTP_STATUS_CODES['ok']>
<cfset variables.instance.dispatcher.getCache().set(this, response)>
</cfif>
</cfif>
<!--- return the response object --->
<cfreturn response>
</cffunction>
@@ -16,7 +16,13 @@
response_file = "",
response_uri = "",
errors = [],
request = createobject("component", "restfulcf.framework.core.Request")
request = createobject("component", "restfulcf.framework.core.Request"),
cache = {
active = FALSE,
expiry = 0,
hit = FALSE,
key = ""
}
}>
<cffunction name="init" access="public" returntype="restfulcf.framework.core.Response" output="no" hint="I am the encapsulation of a request response">
@@ -84,6 +90,35 @@
<cfreturn createobject("component", "restfulcf.framework.core.ErrorCollection").init(variables.instance.errors)>
</cffunction>
<cffunction name="getCacheStatus" access="public" returntype="boolean" output="no" hint="Gets if this response should be cached">
<cfreturn variables.instance.cache.active>
</cffunction>
<cffunction name="setCacheStatus" access="public" returntype="void" output="no" hint="Sets if this response should be cached">
<cfargument name="cache" type="boolean" required="yes" hint="Should we cache this response?">
<cfset variables.instance.cache.active = arguments.cache>
</cffunction>
<cffunction name="getCacheExpiry" access="public" returntype="boolean" output="no" hint="Gets how long this response should be cached for">
<cfreturn variables.instance.cache.expiry>
</cffunction>
<cffunction name="setCacheExpiry" access="public" returntype="void" output="no" hint="Sets how long this response should be cached for">
<cfargument name="timespan" type="numeric" required="yes" hint="The time in days to cache the data for (use `createtimespan`)">
<cfset variables.instance.cache.expiry = arguments.timespan>
</cffunction>
<cffunction name="getCacheHit" access="public" returntype="boolean" output="no" hint="Gets if this response was returned from the cache">
<cfreturn variables.instance.cache.hit>
</cffunction>
<cffunction name="setCacheHit" access="public" returntype="void" output="no" hint="Sets if this response was returned from the cache">
<cfargument name="hit" type="boolean" required="yes" hint="Was this a cached response?">
<cfset variables.instance.cache.hit = arguments.hit>
</cffunction>
<cffunction name="getCacheKey" access="public" returntype="string" output="no" hint="Returns the cache key used for this response">
<cfreturn variables.instance.cache.key>
</cffunction>
<cffunction name="setCacheKey" access="public" returntype="void" output="no" hint="Sets the cache key used for this response">
<cfargument name="key" type="string" required="yes" hint="The cache key">
<cfset variables.instance.cache.key = arguments.key>
</cffunction>
<cffunction name="getRequest" access="public" returntype="restfulcf.framework.core.Request" output="no" hint="Returns the request object that resulted in this response">
<cfreturn variables.instance.request>
</cffunction>
@@ -0,0 +1,116 @@
<!--- -->
<fusedoc fuse="restfulcf/framework/core/cache/AbstractCache.cfc" language="ColdFusion" specification="2.0">
<responsibilities>
I am the base of all response caches
</responsibilities>
</fusedoc>
--->
<cfcomponent output="no">
<cfset variables.default_timeout = createtimespan(0,0,30,0)>
<cffunction name="init" access="public" returntype="restfulcf.framework.core.cache.AbstractCache" output="no" hint="Initialises this cache">
<cfargument name="timeout" type="numeric" required="no" hint="The timespan to use as a default timeout">
<!--- the base component is an 'abstract' cache and cannot be used as an actual implementation --->
<cfif getmetadata(this).name EQ "restfulcf.framework.core.cache.AbstractCache">
<cfthrow type="RESTfulCF.AbstractCache.CannotInitAbstractCache" message="The #getmetadata(this).name# component must be extended to provide a concrete cache implementation." detail="Extend this component and override the getKey, setKey and deleteKey functions.">
</cfif>
<cfif structkeyexists(arguments, "timeout")><cfset variables.default_timeout = arguments.timeout></cfif>
<cfreturn this>
</cffunction>
<cffunction name="get" access="public" returntype="struct" output="no" hint="Get a response from the cache. Returns a struct with up to two keys: `found` = true|false; `response` = the response object (will only exist if key is found)">
<cfargument name="request" type="restfulcf.framework.core.Request" required="yes" hint="The request object to get a response for">
<cfset var resp = { found = FALSE }>
<cfset var cache_key = serialiseRequest(arguments.request)>
<cfset var cache_data = getKey(cache_key)>
<cfif len(cache_data)>
<cfset resp.found = TRUE>
<cfset resp.response = deserialiseResponse(cache_data, arguments.request)>
<cfset resp.response.setCacheHit(TRUE)>
<cfset resp.response.setCacheKey(cache_key)>
</cfif>
<cfreturn resp>
</cffunction>
<cffunction name="set" access="public" returntype="void" output="no" hint="Sets a key in the cache">
<cfargument name="request" type="restfulcf.framework.core.Request" required="yes" hint="The request object to set a response for">
<cfargument name="response" type="restfulcf.framework.core.Response" required="yes" hint="The response object to set">
<!--- if the cache expiry stamp in the response is zero, use the default --->
<cfset var exp = arguments.response.getCacheExpiry()>
<cfif NOT exp><cfset exp = variables.default_timeout></cfif>
<cfset setKey(
key = serialiseRequest(arguments.request),
value = serialiseResponse(arguments.response),
expires = dateadd('s', round(exp * 86400), now())
)>
</cffunction>
<cffunction name="delete" access="public" returntype="void" output="no" hint="Deletes a key from the cache">
<cfargument name="request" type="restfulcf.framework.core.Request" required="yes" hint="The request object to delete the cached response for">
<cfset deleteKey(serialiseRequest(arguments.request))>
</cffunction>
<cffunction name="serialiseRequest" access="private" returntype="string" output="no" hint="Builds a key from a request object">
<cfargument name="request" type="restfulcf.framework.core.Request" required="yes" hint="The request object to build the cache key for">
<cfset var args = []>
<cfset var key = []>
<cfset var arg = "">
<!--- build the structure that will represent this request --->
<cfset var params = {}>
<cfset params['_verb'] = arguments.request.getRoute().getVerb()>
<cfset params['_uri'] = arguments.request.getRoute().getURI()>
<cfset params['_type'] = arguments.request.getResponseType()>
<cfset structappend(params, arguments.request.getArguments())>
<!--- build what is effectively an alpha-ordered query string of all params --->
<cfset args = structkeyarray(params)>
<cfset arraysort(args, "textnocase")>
<cfloop array="#args#" index="arg"><cfset arrayappend(key, lcase(arg) & "=" & params[arg])></cfloop>
<cfreturn arraytolist(key, "&")>
</cffunction>
<cffunction name="serialiseResponse" access="private" returntype="string" output="no" hint="Builds a cached response string from a response object">
<cfargument name="response" type="restfulcf.framework.core.Response" required="yes" hint="The response object for serialisation">
<cfset var resp = {
status_code = arguments.response.getStatusCode(),
status_text = arguments.response.getStatusText(),
response_type = arguments.response.getResponseType(),
response_uri = arguments.response.getResponseURI(),
response_body = arguments.response.getResponseBody(),
response_file = arguments.response.getResponseFile()
}>
<cfreturn serializejson(resp)>
</cffunction>
<cffunction name="deserialiseResponse" access="private" returntype="restfulcf.framework.core.Response" output="no" hint="Builds a response object from a cached response string">
<cfargument name="data" type="string" required="yes" hint="The string representation of the reponse to build in to a complete response object">
<cfargument name="request" type="restfulcf.framework.core.Request" required="yes" hint="The request object that requested this cached response">
<cfset var response = createobject("component", "restfulcf.framework.core.Response").init(arguments.request)>
<cfset var resp = deserializejson(arguments.data)>
<cfset response.setStatusCode(resp.status_code)>
<cfset response.setStatusText(resp.status_text)>
<cfset response.setResponseType(resp.response_type)>
<cfset response.setResponseURI(resp.response_uri)>
<cfset response.setResponseBody(resp.response_body)>
<cfset response.setResponseFile(resp.response_file)>
<cfset response.setCacheHit(TRUE)>
<cfreturn response>
</cffunction>
<!--- THESE FUNCTIONS MUST BE IMPLEMENTED IN THE CONCRETE CACHE --->
<cffunction name="getKey" access="public" returntype="string" output="no" hint="Gets the value for a given key from the cache">
<cfargument name="key" type="string" required="yes" hint="The key to retrieve">
<cfthrow type="RESTfulCF.AbstractCache.NotImplemented" message="The getKey() function must be implemented by #getmetadata(this).name#">
</cffunction>
<cffunction name="setKey" access="public" returntype="void" output="no" hint="Sets a given value for a given key in the cache">
<cfargument name="key" type="string" required="yes" hint="The key to set">
<cfargument name="value" type="string" required="yes" hint="The value to store">
<cfargument name="expires" type="numeric" required="yes" hint="The cache expiry date time">
<cfthrow type="RESTfulCF.AbstractCache.NotImplemented" message="The setKey() function must be implemented by #getmetadata(this).name#">
</cffunction>
<cffunction name="deleteKey" access="public" returntype="void" output="no" hint="Deletes a given key from the cache">
<cfargument name="key" type="string" required="yes" hint="The key to delete">
<cfthrow type="RESTfulCF.AbstractCache.NotImplemented" message="The deleteKey() function must be implemented by #getmetadata(this).name#">
</cffunction>
</cfcomponent>
@@ -0,0 +1,49 @@
<!--- -->
<fusedoc fuse="restfulcf/framework/core/cache/ApplicationCache.cfc" language="ColdFusion" specification="2.0">
<responsibilities>
I am a concrete implementation of an abstract cache that uses the application scope.
NOTE: You probably don't want to use this: there's no reaper, so it could end up being
quite a memory hog, plus it's not the most efficient. Just use it as an example of
what you need to do to create your own concrete cache (using memcached or something
similar, maybe...)
</responsibilities>
</fusedoc>
--->
<cfcomponent extends="restfulcf.framework.core.cache.AbstractCache" output="no">
<cffunction name="init" access="public" returntype="restfulcf.framework.core.cache.ApplicationCache" output="no" hint="Initialises this cache">
<cfargument name="timeout" type="numeric" required="no" hint="">
<cfset super.init(argumentcollection=arguments)>
<cfif NOT structkeyexists(application['_restfulcf'], application.applicationname & "_cache")>
<cfset application['_restfulcf'][application.applicationname & "_cache"] = {}>
</cfif>
<cfset variables.cache = application['_restfulcf'][application.applicationname & "_cache"]>
<cfreturn this>
</cffunction>
<cffunction name="getKey" access="public" returntype="string" output="no" hint="Get a response from the cache">
<cfargument name="key" type="string" required="yes" hint="The key to retrieve">
<cfset var resp = "">
<cfif structkeyexists(variables.cache, arguments.key)>
<cfif structkeyexists(variables.cache[arguments.key], "expires") AND variables.cache[arguments.key].expires GT NOW()>
<cfset resp = variables.cache[arguments.key].value>
<cfelse>
<cfset deleteKey(arguments.key)>
</cfif>
</cfif>
<cfreturn resp>
</cffunction>
<cffunction name="setKey" access="public" returntype="string" output="no" hint="Sets a given value for a given key in the cache">
<cfargument name="key" type="string" required="yes" hint="The key to set">
<cfargument name="value" type="string" required="yes" hint="The value to store">
<cfargument name="expires" type="numeric" required="yes" hint="The cache expiry date time">
<cfset variables.cache[arguments.key] = { value = arguments.value, expires = arguments.expires }>
</cffunction>
<cffunction name="deleteKey" access="public" returntype="void" output="no" hint="Deletes a given key from the cache">
<cfargument name="key" type="string" required="yes" hint="The key to delete">
<cfset structdelete(variables.cache, arguments.key)>
</cffunction>
</cfcomponent>
@@ -0,0 +1,16 @@
<!--- -->
<fusedoc fuse="restfulcf/framework/core/cache/EmptyCache.cfc" language="ColdFusion" specification="2.0">
<responsibilities>
I am a concrete implementation of an abstract cache which doesn't actually cache anything.
This is the default cache instance for the framework.
</responsibilities>
</fusedoc>
--->
<cfcomponent extends="restfulcf.framework.core.cache.AbstractCache" output="no">
<cffunction name="getKey" access="public" returntype="string" output="no" hint="Get a response from the cache"><cfreturn ""></cffunction>
<cffunction name="setKey" access="public" returntype="string" output="no" hint="Sets a given value for a given key in the cache"></cffunction>
<cffunction name="deleteKey" access="public" returntype="void" output="no" hint="Deletes a given key from the cache"></cffunction>
</cfcomponent>
Oops, something went wrong.

0 comments on commit fb56056

Please sign in to comment.