Learning ColdFusion 8: Ping - User Defined Function (Inspired By Ray Camden)
In his comments posted to my "Set It And Forget It" CFThread tutorial, Ray Camden expressed interest in a ColdFusion user defined function that could be fired for Ping support in a similar, asynchronous manner). I am not one to take suggestions lightly, so I figured I would give this ColdFusion user defined function a shot. But more than just the fun of coding, this UDF gives us the opportunity to explore some new, very exciting features of ColdFusion 8:
- Implicit struct creation (notice the LOCAL scope)
- AttributeCollection tag attribute
- CFThread
That being said, here is the ColdFusion user defined function, Ping():
<cffunction
name="Ping"
access="public"
returntype="void"
output="false"
hint="Pings the given URL in an asynchronous fashion. Will download the result if a file name is given.">
<!--- Define arguments. --->
<cfargument
name="URL"
type="string"
required="true"
hint="The URL to be called using CFHttp."
/>
<cfargument
name="File"
type="string"
required="false"
default=""
hint="File to which the CFHttp binary result will be stored."
/>
<!--- Define the local scope. --->
<cfset var LOCAL = {} />
<!---
Create the attributes collection that we are
going to use when firing the CFHttp tag.
--->
<cfset LOCAL.Attributes = {} />
<!--- Set the URL. --->
<cfset LOCAL.Attributes.URL = ARGUMENTS.URL />
<!---
Set the method. In this case, we are going to default
to HEAD since we don't care about the response body
unless we are goint to download the file.
--->
<cfset LOCAL.Attributes.Method = "HEAD" />
<!--- Set the user agent. --->
<cfset LOCAL.Attributes.UserAgent = "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.4) Gecko/20070515 Firefox/2.0.0.4" />
<!---
Check to see if a file name was given for
the binary download.
--->
<cfif ARGUMENTS.File.Length()>
<!---
Set the method to GET since we are going
to be downloading the file - we need to
have a response body.
--->
<cfset LOCAL.Attributes.Method = "GET" />
<!--- Flag for the binary get. --->
<cfset LOCAL.Attributes.GetAsBinary = "yes" />
<!--- Give the path for the file. --->
<cfset LOCAL.Attributes.Path = GetDirectoryFromPath(
ARGUMENTS.File
) />
<!--- Give the file name to store. --->
<cfset LOCAL.Attributes.File = GetFileFromPath(
ARGUMENTS.File
) />
<!---
There is a chance that only a directory was
supplied. If this is the case, try to grab the
file name from the URL as the target file.
--->
<cfif (
(NOT LOCAL.Attributes.File.Length()) AND
(GetFileFromPath( ARGUMENTS.URL ).Length())
) >
<!--- Grab the file name from the URL. --->
<cfset LOCAL.Attributes.File = GetFileFromPath(
ARGUMENTS.URL
) />
</cfif>
</cfif>
<!---
When using the CFHttp tag, we are going to need to
use a CFTry / CFCatch since we might be storing the
result to a file (plus to catch any other error that
might arise, like a timeout).
--->
<cftry>
<!---
When executing our CFHttp call, we are going to
wrap it up in a CFThread tag to allow us to set
it and forget it in an asynchronous manner.
When launching the CFThread tag, we need to pass
in any variables that are NOT stored in the
VARIABLES scope. In this case, we need to pass
in both the attribute collection structures as
well as the referer that we want to use in the
CFHttp call.
--->
<cfthread
action="run"
name="cfhttp_#CreateUUID()#"
attributes="#LOCAL.Attributes#"
referer="#ARGUMENTS.URL#">
<!---
Fire the CFHttp tag using our attributes
collection. Remember, when we use the
attributes collection, no other attributes
can be used.
--->
<cfhttp
attributecollection="#ATTRIBUTES.Attributes#">
<!---
Pass in the target as the referer. This
will just help make sure no security
issues block us.
--->
<cfhttpparam
type="CGI"
name="referer"
value="#ATTRIBUTES.Referer#"
/>
</cfhttp>
</cfthread>
<cfcatch>
<!--- An error occurred. --->
</cfcatch>
</cftry>
<!--- Return out. --->
<cfreturn />
</cffunction>
This Ping method has two options: if you send in just a URL to ping, it will launch an asynchronous CFHttp request for that URL's header information. This will allow a more efficient ping that will execute the file, but not return a message body (remember, in this asynchronous scenario, we don't care about a return value). You can also send in a file path. In doing so, the Ping UDF will switch over from a HEAD method to a GET method in which the response content is stored as a binary object and saved to the given file (still in an asynchronous manner).
If the file path you supply does not have a file name, then the UDF will attempt to use the file name of the target URL. If neither is available, then I think CFHttp will fail, but since it's being executed in a different thread, we will not see this error; remember, CFThread-encapsulated code cannot output content to the parent page's content buffer in any direct way.
Here are the two different method of invocation:
<!---
Ping and download photo to the given
directory. Ping() will use the target file name.
--->
<cfset Ping(
URL = "http://some-image-domain.com/kittens.jpg",
File = ExpandPath( "./data/" )
) />
<!--- Ping URL, but to not save file. --->
<cfset Ping(
URL = "http://www.fullasagoog.com/googimporter.cfm?id=XXX"
) />
One of the more subtle, tricky aspects of the UDF is giving the CFHttp tag access to the attribute collection we created for it. Since the CFHttp tag is located inside of our CFThread tag, it means that it only has access to the VARIABLES scope; however, since our attribute collection was created in the function's explicit LOCAL scope, CFHttp will not have default access to these values. In order to get that collection (and the referer) to the CFHttp tag, we have to pass these values in as attributes to the CFThread tag. Doing this will make those values available in the ATTRIBUTES scope of the CFThread body.
As a side note, notice how easy the AttributeCollection attribute makes it for us to create dynamic CFHttp tags. Before the introduction of the Coldfusion 8 AttributeCollection attribute, we would have had to have a CFIF / CFELSE statement for the downloading and non-downloading version of the CFHttp tag. Both of those CFHttp tags would have had 90% the same attributes and CFHttpParam tag - a very inefficient way of doing things. By using the AttributeCollection, we can dynamically include tag attributes without having to duplicate any effort.
ColdFusion 8 is going to be a beautiful thing!
Want to use code from this post? Check out the license.
Reader Comments
Why are you talking to you in third party?
Cause who else will listen to my nonsense?
Looks to me to have some first person in there too.
I don't know about listening to you, but I enjoy reading your blog. One thing I have noticed is most programmers don't know how to write, other than code.
@Gary,
Thanks for reading my stuff, I am glad you like it.... I am hoping that since you enjoy my writings than you were implying that I was one of the coders that can write well??? Of course, if you were not, no worries - I am a code-monkey at heart (but I did take both creative writing Fiction and Poetry) :)
Ping uses ICMP to determine whether a "host" is reachable. Your UDF does nothing of the sort. You're convoluting cfhttp with ping. Not cool.
@Daniel,
I am not performing the same kind of Ping action you would from the command line. By "ping" here, I am using the more generic sense of the word - to make contact. Think about it in terms of a conversation - sometimes you might here someone say "Ping me back in a few days once you've read over the documents"... they are not talking about ICMP nonsense - they just mean make contact. That is the way in which I am using it.