In this post, we're going to learn, how to create an event gateway in Lucee. Before that, we should know about, what is an Event gateway.
In common, An event gateway is a program, which is continuously monitoring a resource & do necessary actions on certain events. In Lucee, we also have the similar process available. Recent versions of Lucee come up with two built-in event gateways ( Directory watcher & Mail watcher ). But we could create our own event gateways for handling any special scenarios like SASS compilation etc.
Requirements:
- A Gateway component
- A Gateway Driver component
- A Listener component ( Optional )
Of Course, we should place these files in right location & each file should be in some specific structure. So, i'll mention the right locations, whenever it is needed. Just remember the directory mappings below.
- {{lucee-web}} : this is the WEB-INF folder of webroot.
- {{lucee-server}} : this is the folder, where Lucee has been installed.
A Gateway component:
It should be placed in "{{lucee-server}}/context/context/admin/gdriver/demoGateway.cfc".
This is the file, which is responsible for doing the actions whatever we need. This component must contain some public functions inside that.
- init function, which receives the all configurations for the event gateway.
- start function, which will be continuously running in the background based on a config "variables.state".
- A stop and restart functions to stop/restart the event gateway from Admin interface.
- A getState function, which returns the current state of the gateway instance (running,stopping,stopped).
- A sendMessage function, to manually call the event gateway from code using sendGatewayMessage().
Additionally you might need to create some private functions for error logging etc. Here is the sample code for demoGateway.cfc
demoGateway.cfc
<cfcomponent extends="Gateway">
<cfset fields=array(
field("Directory","directory","",true,"The directory you want to watch","text")
,field("Watch subdirectories","recurse","true",true,"Should we watch the directory and all subdirectories too","checkbox")
,field("Interval (ms)","interval","60000",true,"The interval between checks, in miliseconds","text")
,field("File filter","extensions","*",true,"The comma separated list of file filters to match (* = all files). Examples: *user*,*.gif,2010*,myfilename.txt","text")
,group("CFC Listener Function Definition","Definition for the CFC Listener Functions, when empty no listener is called",3)
,field("Change","changeFunction","onChange",true,"called when a file change","text")
,field("Add","addFunction","onAdd",true,"called when a file is added","text")
,field("Delete","deleteFunction","onDelete",true,"called when a file is removed","text")
)>
<cffunction name="getClass" returntype="string">
<cfreturn "">
</cffunction>
<cffunction name="getCFCPath" returntype="string">
<cfreturn "lucee.extension.gateway.demoDriver">
</cffunction>
<cffunction name="getLabel" returntype="string" output="no">
<cfreturn "Directory watcher ( Demo )">
</cffunction>
<cffunction name="getDescription" returntype="string" output="no">
<cfreturn "Simply logs the changed file with its time">
</cffunction>
<cffunction name="onBeforeUpdate" returntype="void" output="false">
<cfargument name="cfcPath" required="true" type="string">
<cfargument name="startupMode" required="true" type="string">
<cfargument name="custom" required="true" type="struct">
</cffunction>
<cffunction name="getListenerCfcMode" returntype="string" output="no">
<cfreturn "required">
</cffunction>
<cffunction name="getListenerPath" returntype="string" output="no">
<cfreturn "lucee.extension.gateway.demoGatewayListener">
</cffunction>
</cfcomponent>A Driver component
It should be placed in {{lucee-web}}/lucee/components/lucee/extension/gateway/demoDriver.cfc.
The driver is used to configure and define your Gateway. Using this, you can define the form fields in the Lucee admin settings page for your gateway. If you need more configurations, you might need to add them as fields array in this driver component. Also it makes sure that your gateway is listed as an available Gateway in the Lucee Admin page.
- getClass():string
- Returns the java class name.
- If the gateway is Java based, then the java class has to implement the interface "org.opencfml.eventgateway.Gateway".
- If it is not java based, then this method must return an empty string or void.
- getCFCPath():string
- Returns the cfc path, when the gateway is cfc based.
- If it is not cfc based, then this method must return an empty string or void.
- getLabel():string
- The label (friendly name) of the gateway.
- getDescription():string
- The description of the gateway
- onBeforeUpdate(string cfcPath, string startupMode, struct custom):void
- This method is invoked before the settings entered in the form are saved.
- This method can be used to validate the entered data.
- onBeforeError(cfcatch):void
- Invoked before an error is thrown.
- Can be used to throw your own error, and/or do logging.
- getListenerCfcMode():string.
- It should be "none", if no listener is defined.
- It should be "required", if we're defining a listener for the gateway.
- getListenerPath():string
- returns the config, location of a listener cfc.
- returns the config, location of a listener cfc.
demoDriver.cfc
<cfcomponent output="no">
<cfset variables.logFileName = "demoLogGateway" />
<cfset variables.state = "stopped" />
<cffunction name="init" access="public" output="no" returntype="void">
<cfargument name="id" required="false" type="string">
<cfargument name="config" required="false" type="struct">
<cfargument name="listener" required="false" type="component">
<cfset variables.id = id>
<cfset variables.config = config>
<cfset variables.listener = listener>
</cffunction>
<cffunction name="start" access="public" output="no" returntype="void">
<cfset var sleepStep = iif(
variables.config.interval lt 500,
'variables.config.interval',
de(500)
) />
<cfset var i = -1 />
<cfwhile variables.state EQ "stopping">
<!--- Loop & wait for 10 ms, until gateway is running --->
<cfset sleep(10)>
</cfwhile>
<cfset variables.state = "running">
<cfset variables._filter = cleanExtensions(
variables.config.extensions
) />
<cfset logDetails("start", "information")>
<cfset var funcNames = {
add : config.addFunction,
change : config.changeFunction,
delete : config.deleteFunction
}>
<cfif not DirectoryExists(variables.config.directory)>
<cfset logDetails(
"Directory [#variables.config.directory#] does not exist or is not a directory",
"error"
)>
</cfif>
<cfif not StructKeyExists(variables.config,"recurse")>
<cfset variables.config.recurse = false>
</cfif>
<cfset var files = loadFiles(
variables.config.directory,
variables.config.recurse,
variables._filter
) />
<cfwhile variables.state EQ "running">
<cfset var coll = compareFiles(
files,
funcNames,
config.directory,
config.recurse,
variables._filter
)>
<cfset files = coll.data>
<cfset var name = "">
<cfset var funcName = "">
<cfloop collection="#coll.diff#" item="name">
<cfset funcName = coll.diff[name].action>
<cfif len(funcName)>
<cfset logDetails(
"change:::#funcName#:::#name#:::start",
"information"
)>
<cfset variables.listener[funcName](
coll.diff[name]
)>
<cfset logDetails(
"change:::#funcName#:::#name#:::done",
"information"
)>
</cfif>
</cfloop>
</cfwhile>
</cffunction>
<cffunction
name="loadFiles"
access="private"
output="no"
returntype="struct">
<cfargument name="directory" type="string" required="yes">
<cfargument name="recurse" type="boolean" required="no" default="#false#">
<cfargument name="fileFilter" type="string" required="no" default="*" />
<cfset var dir = getFiles(
arguments.directory,
arguments.recurse,
arguments.fileFilter
) />
<cfset var sct = {} />
<cfloop query="dir">
<cfset sct[
dir.directory & server.separator.file & dir.name
] = createElement(dir) />
</cfloop>
<cfreturn sct />
</cffunction>
<cffunction
name="getFiles"
access="private"
output="no"
returntype="query">
<cfargument name="directory" type="string" required="yes">
<cfargument name="recurse" type="boolean" required="no" default="false" />
<cfargument name="fileFilter" type="string" required="no" default="*" />
<cfset var qDir = "" />
<cfdirectory
directory="#arguments.directory#"
action="list"
name="qDir"
type="file"
filter="#arguments.fileFilter#"
recurse="#arguments.recurse#" />
<cfreturn qDir />
</cffunction>
<cffunction
name="stop"
access="public"
output="no"
returntype="void">
<cfset logDetails("stop", "information")>
<cfset variables.state = "stopping">
</cffunction>
<cffunction
name="getState"
access="public"
output="no"
returntype="string">
<cfreturn variables.state />
</cffunction>
<cffunction
name="logDetails"
returntype="void"
access="private"
output="no">
<cfargument name="content" required="yes" type="string" />
<cfargument name="type" required="yes" type="string" />
<cflog
text="#arguments.content#"
type="#arguments.type#"
file="#variables.logFileName#" />
</cffunction>
</cfcomponent>A Listener component ( Optional )
It should be placed in {{lucee-web}}/lucee/components/lucee/extension/gateway/demoGatewayListener.cfc.
Listener is component which is used to make the Gateway file as small as we can. If an event occurs for the gateway, it should call a corresponding function from the listener component.
demoGatewayListener.cfc
<cfcomponent>
<cfset variables.logFileName = "demoLogGateway" />
<cffunction name="checkChange" access="public" output="no" returntype="void">
<cfargument name="data" type="struct" required="yes">
<cfset logDetails("change:#serialize(data)#", "information")>
</cffunction>
<cffunction name="logDetails" returntype="void" access="private" output="no">
<cfargument name="content" required="yes" type="string" />
<cfargument name="type" required="yes" type="string" />
<cflog text="#arguments.content#" type="#arguments.type#" file="#variables.logFileName#" />
</cffunction>
</cfcomponent>Once all the files are placed in right location, we're all set to create the event gateway instance from Lucee web administrator under menu "Services --> Event Gateway".
While creating the gateway instance from the web administrator, you might need to enter the details that are required for the event gateway as per the fields configuration in driver component.
For more details about creating Event gateways for a specific task, please check one of our blog post about SASS compilation.
