StructureCMS

January 27, 2012

Using the SmarterStats Query API service in ColdFusion

Filed under: ColdFusion, Programming — joel.cass @ 3:53 pm

I must admit that it’s been a while since I’ve posted anything in this blog. To be honest, it’s mainly because there’s been nothing remarkable to post about. Nothing anyone would benefit from knowing, really.

It’s even to the point that my role involves much more than just programming these days – recently I have been involved in reviewing web statistics packages in the hope of landing on a solution. Well, the solution is found in SmarterStats – quite a remarkable web statistics package that does most things. It even has an API.

About that API – it has a largely undocumented ‘Query’ webservice. It is not enabled by default, but once enabled it allows you to query the statistics data via a web service.

To enable the service, you will need to create an authorisation key. A good one to use is a UUID. Then, you need to define that key in [SmarterStats_Root]\MRS\App_Data\Config\AppConfig.xml:

    <LocalHostDeleted>false</LocalHostDeleted>
++  <WebServiceAuthorizationCode>nnnnnnnn-nnnn-nnnn-nnnnnnnnnnnnnnnn</WebServiceAuthorizationCode>
    <ExpirationNotification />

Then you can access the service via http://localhost:9999/Services/Query.asmx, WSDL path http://localhost:9999/Services/Query.asmx?wsdl

The query service uses some form of psuedo-sql that runs a query against what looks like a function result, ie. SELECT * FROM fTableName(siteId, dateFrom, dateTo, maxItems, extraParam**) WHERE blah=blah ORDER BY blah.

** Parameters maxItems and extraParam are undocumented and were found by chance. MaxItems is pretty obvious, it’s the number of records to retrieve. Use extraParam to filter certain queries by page. Only certain queries support the extra parameter so try them out.

A list of tables is available in the file [SmarterStats_Root]\MRS\App_Data\Config\ReportConfig.xml – you can copy this to your development directory and then use it to generate a list of available reports.

Here are some example reports you can call using the web service (assume site id = 1):

  • Get Daily Site Traffic
    SELECT * FROM fActivityTotalTrend(1, '2012-01-01T00:00:00', '2012-01-27T00:00:00', 50)
  • Get Daily traffic to home page
    SELECT * FROM fDailyActivityForFile(1, '2012-01-01T00:00:00', '2012-01-27T00:00:00', 20, '/')
  • All pages, ordered by popularity
    SELECT * FROM fTopPages(1, '2012-01-01T00:00:00', '2012-01-27T00:00:00')
  • Top 10 most popular pages
    SELECT * FROM fTopPages(1, '2012-01-01T00:00:00', '2012-01-27T00:00:00', 10)
  • Web browsers
    SELECT * FROM fBrowsers(1, '2012-01-01T00:00:00', '2012-01-27T00:00:00')
  • Visitors by City
    SELECT * FROM fGeographicCountryByCity(1, '2012-01-01T00:00:00', '2012-01-27T00:00:00')
  • Visitors by City, to a certain URL
    SELECT * FROM fGeographicsByFile(1, '2012-01-01T00:00:00', '2012-01-27T00:00:00', 20, '/some/url/')

Calling the web service from coldfusion is pretty easy:

<cfsetting enablecfoutputonly="true">

<!--- 'static' service parameters --->
<cfset strUsername = "username">
<cfset strPassword = "password">
<cfset strAuthCode = "nnnnnnnn-nnnn-nnnn-nnnnnnnnnnnnnnnn">
<cfset strWebServiceUrl = "http://127.0.0.1:9999/Services/Query.asmx?wsdl">

<!--- initialise web service --->
<cfset objWebService = createObject("webservice", strWebServiceUrl)>

<!--- query parameters (you can pass these in from a form) --->
<cfset strReport = "fTopPages">
<cfset numSiteId = 1>
<cfset dateFrom = createDate(year(now()), month(now()), 1)>
<cfset dateTo = createDate(year(now()), month(now()), day(now()))>
<cfset numRows = 20>

<!--- compose query --->
<cfset strQuery = "SELECT * FROM #strReport#(#numSiteId#, '#dateformat(dateFrom,'yyyy-mm-dd')#T00:00:00', '#dateformat(dateTo,'yyyy-mm-dd')#T00:00:00')">

<!--- execute query --->
<cfset dsResult = objWebservice.executeQuery(strUsername, strPassword, strAuthCode, numSiteId, strQuery, numRows)>

<!--- convert to query --->
<cfset qryResult = datasetToQuery(dsResult, 'Table1')><!--- version 7: it is 'Table1', version 6: it is 'results' --->

<!--- output result --->
<cfdump var="#qryResult#">

<!--- FUNCTION TO CONVERT .NET DATASET TO QUERY (added here for convenience - move to helper class) --->

<cffunction name="datasetToQuery" access="public" returntype="query" output="false">
	<cfargument type="any" name="dataset" required="true">
	<cfargument type="string" name="tablename" required="true">

	<!--- dataset has 2 nodes: 1) Column definitions 2) Data --->

	<cfset var qryResult = "">
	<cfset var lstColumns = "">
	<cfset var lstTypes = "">
	<cfset var aryDataset = ARGUMENTS.dataset.get_any()>
	<cfset var aryColumns = XmlSearch(aryDataset[1].getAsString(), "/xs:schema/xs:element[@name='#ARGUMENTS.tablename#']/xs:complexType/xs:sequence/xs:element")>
	<cfset var aryData = XmlSearch(aryDataset[2].getAsString(), "/diffgr:diffgram/NewDataSet/#ARGUMENTS.tablename#")>
	<cfset var i = 0>
	<cfset var c = 0>

	<!--- get columns --->
	<cfloop from="1" to="#arrayLen(aryColumns)#" index="i">
		<cfset lstColumns = listAppend(lstColumns, aryColumns[i].xmlAttributes.name)>
		<cfswitch expression="#aryColumns[i].xmlAttributes.type#">
			<cfcase value="xs:double,xs:long">
				<cfset lstTypes = listAppend(lstTypes, 'double')>
			</cfcase>
			<cfcase value="xs:date">
				<cfset lstTypes = listAppend(lstTypes, 'timestamp')>
			</cfcase>
			<cfdefaultcase>
				<cfset lstTypes = listAppend(lstTypes, 'varchar')>
			</cfdefaultcase>
		</cfswitch>
	</cfloop>

	<!--- create query object --->
	<cfset qryResult = queryNew(lstColumns, lstTypes)>

	<!--- populate query --->
	<cfloop from="1" to="#arrayLen(aryData)#" index="i">
		<cfset queryAddRow(qryResult)>
		<cfloop from="1" to="#arrayLen(aryData[i].xmlChildren)#" index="c">
			<cfset querySetCell(qryResult, aryData[i].xmlChildren[c].xmlName, aryData[i].xmlChildren[c].xmlText)>
		</cfloop>
	</cfloop>

	<cfreturn qryResult>
</cffunction>

<cfsetting enablecfoutputonly="false">

All this was tested in SmarterStats 7.

August 17, 2011

ColdFusion on Linux – Make sure your hostname is correct!

Filed under: ColdFusion, Technology — joel.cass @ 10:52 am

Recently I have been given the task to install ColdFusion on CentOS. Everything went well, Apache installed fine, related dependencies installed fine, even ColdFusion installed fine. Until I tried accessing the site, upon which I was presented with this error:

java.lang.NullPointerException
	at java.lang.String.indexOf(String.java:1733)
	at java.lang.String.indexOf(String.java:1715)
	at jrun.servlet.session.SessionService.getUrlSessionID(SessionService.java:1097)
	at jrun.servlet.ForwardRequest.getRequestedSessionId(ForwardRequest.java:426)
	at jrun.servlet.ForwardRequest.isRequestedSessionIdValid(ForwardRequest.java:467)
	at jrun.servlet.ForwardRequest.getSession(ForwardRequest.java:344)
	at jrun.servlet.ForwardRequest.create(ForwardRequest.java:135)
	at jrun.servlet.JRunRequestDispatcher.invoke(JRunRequestDispatcher.java:253)
	at jrun.servlet.ServletEngineService.dispatch(ServletEngineService.java:543)
	at jrun.servlet.http.WebService.invokeRunnable(WebService.java:172)
	at jrunx.scheduler.ThreadPool$ThreadThrottle.invokeRunnable(ThreadPool.java:428)
	at jrunx.scheduler.WorkerThread.run(WorkerThread.java:66)

I had installed ColdFusion 9, in a similar method to that described here. So, I tried installing ColdFusion 8 following the same method. Still got the error. I then tried installing ColdFusion 9 to use the root user (not recommended). Still got the error.

So I pulled my hair out for a bit, and then started scanning logs, upon which I came across this line:

08/16 23:23:54 error centostemplate.xxxx.xxx.au: centostemplate.xxxx.xxx.au
java.net.UnknownHostException: centostemplate.xxxx.xxx.au: centostemplate.xxxx.xxx.au

…and then it all fell together! The instance I had been given was created off a template, in which the host name was set in /etc/sysconfig/network to ‘centostemplate.xxxx.xxx.au’ – which did not resolve via DNS! So, the easy fix was to map this over in /etc/hosts to localhost, i.e.

127.0.0.1    centostemplate.xxxx.xxx.au

Restart the services, fixed! This is CentOS in my case, but I think if you ever run into this problem on a *nix platform, check your network config and ensure that your configured hostname resolves to an IP address.

December 7, 2010

Configuring ColdFusion to have different JVM Settings per instance

Filed under: ColdFusion, Programming — joel.cass @ 9:08 am

Recently I had upgraded a ColdFusion server from standalone to multiple instance. This was easy – basically a matter of installing a new copy of ColdFusion as multi instance and copying the settings from the standalone instance. However, the issue has now arisen that every app on the server is to run as it’s own instance and if they all share the same settings, there will not be enough memory to run each instance smoothly.

The problem is, that some instances will require more memory while some will require less. Adobe had posted how to do this on their website but it seems to have been deleted recently. Luckily the instructions were still available google cache! I’ve copied the instructions here for later reference. As I will forget…

Basically, it’s three steps:

  1. Open up the JRun/bin directory
  2. Copy jvm.config to jvm_<server_name>.config
  3. Configure the startup script by
    • Windows: use jrunsvc -remove "<service_name>", then jrunsvc -install <server_name> <server_name> "<service_name>" -config jvm_<server_name>.config
    • Linux/Unix/Mac: add -config jvm_<server_name>.config to the startup command, e.g. jrun -start default -config jvm_<server_name>.config

* <server_name> is the name of the folder under the JRun/servers directory that contains the server, e.g “cfusion”
** <service_name> is the name of the service in windows, e.g. “Macromedia JRun cfusion Service”

May 26, 2010

.Net based HTTP Client in ColdFusion?!

Filed under: .net, ColdFusion — joel.cass @ 5:12 pm

I’ve been banging my head up against the metaphorical walls around here for ages trying to get ColdFusion to access websites via a proxy server that only supports NTLM authentication.

Short answer: don’t bother. CFHTTP does not support NTLM Authentication. Most of the Java libraries claiming to do so are hopeless. Support is inconsistent because no-one knows anything about the standard. Except Microsoft.

So, it only came naturally that the best way to solve the issue would be to use .net – and now that ColdFusion has a gateway to .net components, I could actually write something that solves the problem!

So, what I have done is written a wrapper that can be accessed by ColdFusion, and a simple custom tag to finish it off.

Some more information regarding download and implementation is in the Projects section.

February 25, 2010

Rediscovering MySQL

Filed under: ColdFusion, Database — joel.cass @ 9:14 am

Recently I have had to create a reporting system for some server log files. It was sort of an ad-hoc thing; It was really done in a rush because we couldn’t get AWStats to process the files properly and the customer had a really steep deadline to meet.

Anyway, I started off by creating a system that can read in log files following a regular expression. It would then add the contents of these logs to the database for reporting purposes. The code I used is below:

<cfsetting enablecfoutputonly="true" requesttimeout="864000" />

<cfparam name="logpath" default="#expandPath('logs/')#" />
<cfparam name="filter" default="^nc([0-9]*)\.log$" />

<cfdirectory action="list" directory="#logpath#" name="files" sort="name DESC" />

<cfset regex = "^([^ ]*) ([^ ]*) ([^ ]*) (\[[^\]]*\]) ""([^ ]*) (.*) ([^ ]*)"" ([^ ]*) ([^ ]*)$" />
<cfset date_regex = "^\[([0-9]*)/([A-Za-z]*)/([0-9]*)\:([0-9]*)\:([0-9]*)\:([0-9]*).*\]$" />

<cfset month_map = structNew() />
<cfset month_map["Jan"] = "01" />
<cfset month_map["Feb"] = "02" />
<cfset month_map["Mar"] = "03" />
<cfset month_map["Apr"] = "04" />
<cfset month_map["May"] = "05" />
<cfset month_map["Jun"] = "06" />
<cfset month_map["Jul"] = "07" />
<cfset month_map["Aug"] = "08" />
<cfset month_map["Sep"] = "09" />
<cfset month_map["Oct"] = "10" />
<cfset month_map["Nov"] = "11" />
<cfset month_map["Dec"] = "12" />

<cfif structKeyExists(URL, "resetDB")>
  <cfquery datasource="#application.dsn#">
  DROP TABLE #application.tablename#;
  CREATE TABLE #application.tablename#(
    [id] [bigint] NOT NULL,
    [ip] [nvarchar](50) NULL,
    [datetime] [datetime] NULL,
    [url] [ntext] NULL,
    [url_hash] [nvarchar](50) NULL,
    [method] [nvarchar](50) NULL,
    [status] [nvarchar](50) NULL,
    [size] [nvarchar](50) NULL,
    [unknown1] [nvarchar](50) NULL,
    [unknown2] [nvarchar](50) NULL,
  CONSTRAINT [PK_#application.tablename#] PRIMARY KEY CLUSTERED (
    [id] ASC
  ) WITH (
    PAD_INDEX = OFF,
    STATISTICS_NORECOMPUTE = OFF,
    IGNORE_DUP_KEY = OFF,
    ALLOW_ROW_LOCKS = ON,
    ALLOW_PAGE_LOCKS = ON
  ) ON [PRIMARY]
  ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY];

  CREATE NONCLUSTERED INDEX [idx_#application.tablename#_date] ON [dbo].[#application.tablename#] (
    [datetime] ASC
  ) WITH (
    PAD_INDEX = OFF,
    STATISTICS_NORECOMPUTE = OFF,
    SORT_IN_TEMPDB = OFF,
    IGNORE_DUP_KEY = OFF,
    DROP_EXISTING = OFF,
    ONLINE = OFF,
    ALLOW_ROW_LOCKS = ON,
    ALLOW_PAGE_LOCKS = ON
  ) ON [PRIMARY];

  CREATE NONCLUSTERED INDEX [idx_#application.tablename#_ip] ON [dbo].[#application.tablename#] (
    [ip] ASC
  ) WITH (
    PAD_INDEX = OFF,
    STATISTICS_NORECOMPUTE = OFF,
    SORT_IN_TEMPDB = OFF,
    IGNORE_DUP_KEY = OFF,
    DROP_EXISTING = OFF,
    ONLINE = OFF,
    ALLOW_ROW_LOCKS = ON,
    ALLOW_PAGE_LOCKS  = ON
  ) ON [PRIMARY];

  CREATE NONCLUSTERED INDEX [idx_#application.tablename#_url] ON [dbo].[#application.tablename#] (
    [url_hash] ASC
  ) WITH (
    PAD_INDEX  = OFF,
    STATISTICS_NORECOMPUTE  = OFF,
    SORT_IN_TEMPDB = OFF,
    IGNORE_DUP_KEY = OFF,
    DROP_EXISTING = OFF,
    ONLINE = OFF,
    ALLOW_ROW_LOCKS  = ON,
    ALLOW_PAGE_LOCKS  = ON
  ) ON [PRIMARY];
  </cfquery>
</cfif>

<cfloop query="files">

  <cfif REFindNoCase(filter, files.name)>

      <cfset i = 0 />
      <cfset strFile = files.name />
      <cfset strFileId = REReplace(strFile, filter, "\1") />
      <cfset objSystem = CreateObject("java", "java.lang.System") />
      <cfset getMaxId = "" />
      <cfset start_i = 0 />

      <cflock name="GenerateStats_#application.sitename#_#strFileId#" type="exclusive" timeout="10">

        <cflog file="GenerateStats_#application.sitename#" text="#strFile# Started" />

        <cfquery name="getMaxId" datasource="#application.dsn#">
        SELECT
          max(id) as n
        FROM
          #application.tablename#
        WHERE
          id >= <cfqueryparam cfsqltype="CF_SQL_BIGINT" value="1#strFileId##numberFormat(0,'0000000000')#">
        AND
          id <= <cfqueryparam cfsqltype="CF_SQL_BIGINT" value="1#strFileId##numberFormat(9999999999,'0000000000')#">
        </cfquery>

        <cfif getMaxId.recordcount GT 0 AND getMaxId.n GT 0>
          <cfset start_i = getMaxId.n - "1#strFileId#0000000000" />
        </cfif>

        <cflog file="GenerateStats_#application.sitename#" text="#strFile# Log start = #start_i#" />

        <cfloop file="#logpath#\#strFile#" index="line">
          <cfset i = i + 1 />

          <cfif i GT start_i>

            <cfset strId = "1#strFileId##numberFormat(i,'0000000000')#" />

            <cftry>

              <cfset strIp       = REReplaceNoCase(line, regex, "\1") />
              <cfset strUnknown1 = REReplaceNoCase(line, regex, "\2") />
              <cfset strUnknown2 = REReplaceNoCase(line, regex, "\3") />
              <cfset strDatetime = REReplaceNoCase(line, regex, "\4") />
              <cfset strUrl      = REReplaceNoCase(line, regex, "\6") />
              <cfset strMethod   = REReplaceNoCase(line, regex, "\5") />
              <cfset strStatus   = REReplaceNoCase(line, regex, "\8") />
              <cfset strSize     = REReplaceNoCase(line, regex, "\9") />

              <cfset dtDateTime = CreateDateTime(
                REReplaceNoCase(strDatetime, date_regex, "\3"),
                month_map[REReplaceNoCase(strDatetime, date_regex, "\2")],
                REReplaceNoCase(strDatetime, date_regex, "\1"),
                REReplaceNoCase(strDatetime, date_regex, "\4"),
                REReplaceNoCase(strDatetime, date_regex, "\5"),
                REReplaceNoCase(strDatetime, date_regex, "\6")
              ) />

              <cfquery datasource="#application.dsn#">
              INSERT INTO #application.tablename# (
                id,
                ip,
                unknown1,
                unknown2,
                datetime,
                url,
                url_hash,
                method,
                status,
                size
              ) VALUES (
                <cfqueryparam cfsqltype="CF_SQL_BIGINT" value="#strId#" />,
                <cfqueryparam cfsqltype="CF_SQL_VARCHAR" value="#strIp#" />,
                <cfqueryparam cfsqltype="CF_SQL_VARCHAR" value="#strUnknown1#" />,
                <cfqueryparam cfsqltype="CF_SQL_VARCHAR" value="#strUnknown2#" />,
                <cfqueryparam cfsqltype="CF_SQL_TIMESTAMP" value="#dtDateTime#" />,
                <cfqueryparam cfsqltype="CF_SQL_VARCHAR" value="#strUrl#" />,
                <cfqueryparam cfsqltype="CF_SQL_VARCHAR" value="#hash(strUrl)#" />,
                <cfqueryparam cfsqltype="CF_SQL_VARCHAR" value="#strMethod#" />,
                <cfqueryparam cfsqltype="CF_SQL_VARCHAR" value="#strStatus#" />,
                <cfqueryparam cfsqltype="CF_SQL_INTEGER" value="#strSize#" />
              )
              </cfquery>

              <cfcatch>
                <cflog file="GenerateStats_#application.sitename#" text="Error on #strFile# line #i#: #cfcatch.message# (#cfcatch.detail#)" />
              </cfcatch>

            </cftry>

          </cfif>

          <cfif i MOD 500 EQ 0>
            <cfif fileexists(expandPath("stop.file"))>
              <cflog file="GenerateStats_#application.sitename#" text="#strFile# Aborted: Stop file exists." />
              <cfbreak />
            </cfif>
            <cfset objSystem.GC() />
            <cfthread action="sleep" duration="100" />
          </cfif>

          <cfif i MOD 10000 EQ 0>
            <cflog file="GenerateStats_#application.sitename#" text="#strFile# Processed #numberformat(i)# lines" />
          </cfif>

        </cfloop>

        <cflog file="GenerateStats_#application.sitename#" text="#strFile# Completed (#i# lines)" />

      </cflock>

  </cfif>

  <cfif fileexists(expandPath("stop.file"))>
    <cfbreak />
  </cfif>

</cfloop>

<cfsetting enablecfoutputonly="false" />

This was going OK with SQL Server Express. The logs would be imported and then analysed. Whilst it wasn’t blazingly fast, it could import a few hundred meg of logs data in a few minutes so I could start generating reports.

Then one day I got the request to do the same thing but for the company’s most busiest site – their intranet. Each log file exceeded a gig, which often meant over 7 million records per file. Unfortunately, SQL Server Express was not keeping up all too well, it would start off well, processing around 500 records a second, but as time went by this crawled down to <10 records per second, PLUS, after an overnight run I realised that SQL Server stopped accepting records, with the data file blown out to 4GB at just 3 million records. That's only HALF a log file!

I started looking at my options. The company I work at loves SQL Server, so I thought of ways to make it work. Hmm. Storing less fields? Sacrificing my indexes? Those are not good options. Then, I thought back to my old days of using MySQL. I remember how spastic it used to be, joins were always slow, queries had to be arranged so that they would be optimised in a certain way etc etc. But then, my app is only using one table. My queries are basic (for the most part). It was worth a try.

So, I started again. I created the table in InnoDB. That was a bad move, it was slow from the start at only 40 records per sec. But then, I'm the only one using this app. I don't need transactions, why am I using InnoDB? The answer was clear - try MyISAM. So I dropped and recreated the table as MyISAM and restarted the import. Wow - instantly the result was clear - over 1000 records/sec!

Here is the equivalent MySQL code:

	<cfquery datasource=”#application.dsn#”>
	DROP TABLE #application.tablename#;
	</cfquery>
	<cfquery datasource=”#application.dsn#”>
	CREATE TABLE #application.tablename# (
	  id BIGINT UNSIGNED NOT NULL,
	  ip VARCHAR(50) NULL,
	  datetime DATETIME NULL,
	  url TEXT NULL,
	  url_hash VARCHAR(50) NULL,
	  method VARCHAR(50) NULL,
	  status VARCHAR(50) NULL,
	  size VARCHAR(50) NULL,
	  unknown1 VARCHAR(50) NULL,
	  unknown2 VARCHAR(50) NULL,
	  PRIMARY KEY (id),
	  INDEX idx_#application.tablename#_date(datetime),
	  INDEX idx_#application.tablename#_ip(ip),
	  INDEX idx_#application.tablename#_url(url_hash)
	) ENGINE=MyISAM;
	</cfquery>

The import has now been running overnight and there is no performance degradation. Imports are still running between 800-1000 records/sec and the table now contains 48 million records! And the file size? 10GB including indexes - this is only 2 and a half times the SQL Server file yet it is holding 16 times the amount of data! AND I have just started running queries - queries that took over 60 seconds to execute in SQL server on less than a million rows are now taking a similar time but I have over 48 million records in MySQL.

I think this is a perfect case of the right tool for the right purpose. MySQL - you're a life saver.

December 18, 2009

Bypass Friendly Internet Explorer 404 Messages

Filed under: ColdFusion, PHP — joel.cass @ 9:22 am

I had to build a custom 404 page today and was bashing my head against a wall regarding Internet Explorer’s friendly “The webpage cannot be found” error message page.

Well, according to this link, the solution is simple – the page has to be at least 512 bytes. Too easy.

Of course, you can simply just add 512 bytes of whitespace and/or reduce the amount of whitespace to 512 minus the original size of the page. If your page is greater than 512 bytes you have nothing to worry about.

The ColdFusion repeatString and PHP str_repeat methods would be useful for this.

December 2, 2009

Measuring disk space in Coldfusion

Filed under: ColdFusion — joel.cass @ 2:15 pm

If you’re supporting any legacy applications, then it’s probably only a matter of time until you will get that call early in the morning… “Hi, this is technical support, your server’s down. It seems to have run out of space on drive X…”.

This could have easily been avoided if you had set up some sort of alert. The should be easy right, as easy as using a cfdirectory tag… Oh wait, hold on, other than a recursive listing and summary being extremely inefficient, cfdirectory doesn’t tell you how much space is left on your drive.

Well, we’re all lucky that ColdFusion is built on top of Java. As it turns out, since JDK 6.0 Java has exposed drive information via the java.io.File library.

For example, if you wanted to get a list of all drives on the server, you could use the method File.listRoots():

<cfset objFile = createObject("java","java.io.File")>
<cfset aryRoots = objFile.listRoots()>

<cfset lstDrives = "">

<cfloop from="1" to="#arrayLen(aryRoots)#" index="i">
    <cfset lstDrives = listAppend(lstDrives, aryRoots[i].getPath())>
</cfloop>

Dumping out #lstDrives# would return something like “A:\,C:\,D:\” etc.

From that, you can then get the amount of space available:

<cfset objFile = createObject("java","java.io.File").init("C:\")>

<cfset stcReturn = structNew()>
<cfset stcReturn.freeSpace = objFile.getFreeSpace()>
<cfset stcReturn.totalSpace = objFile.getTotalSpace()>
<cfset stcReturn.readAccess = objFile.canRead()>
<cfset stcReturn.writeAccess = objFile.canWrite()>

If you wanted to set up an alert, you could use the readAccess / writeAccess flags to determine whether the drive should be checked. Chances are that if a drive cannot be read or written it is most probably a CD drive or an external media drive that is empty.

August 18, 2008

Is ColdFusion Dead?

Filed under: ColdFusion, Web Development — Tags: — joel.cass @ 7:32 am

I have been a long term developer and supporter of the ColdFusion cause. I have been developing in the language for more than 10 years and have come to appreciate it as a convenient and reliable way to build web applications.

What is ColdFusion, may you ask? Well it is an application server language. In it’s day, people paid for this kind of thing, but these days application server languages are mainly free and/or cheap to install. However, Cold Fusion is not free, and it’s not that cheap. Anyway, that is beside the point.

The thing is, ColdFusion has not changed much in the last 5 years, well not in my mind, at least. When ColdFusion MX came out in 2004 it was groundbreaking, extraordinary. The whole thing was re-written in Java. While it was not as fast as it’s predecessors it became reliable, and so many new things were added on – component support, web services, native Java support – it was such a breakthrough that it made any later release seem like a bugfix more than anything else.

And in my mind that is exactly what CF7 was – a better working version of ColdFusion MX. Sure, it had some fluffy cfdocument tags thrown in and some minor syntax improvements but nothing revolutionary. CF8 introduced .net support and image manipulation, as well as better performance, but let’s face it, most people were using .net via web services (which is more standardised albeit less efficient) and using tools like CFC_Image to manipulate images. Furthermore, developers had started to adapt their practices to match the performance limitations of CF7, which, involved implementation of best practices anyway. So, CF8 wasn’t of any big help, to me at least.

It leads me to consider that Adobe has effectively “shelved” ColdFusion – some people are baffled by this concept but let me explain. Consider that Adobe sells 1000 copies of ColdFusion every year – lets face it that even this figure is ambitious considering that most people don’t need to buy a copy of CF every year and that many new projects are looking to start in Java or .net these days. Anyway, 1000 copies equals around 4 million dollars, given that all copies are the enterprise version.

However, Adobe would only receive half of this due to retail markup and distribution costs. Out of this comes marketing, admin, and support costs, which might leave around a million dollars for Adobe to play with. Consider that the average developer costs 100K per year and say that they have 5 or 6. Then there might be a manager or two, or even a tech lead who gets a little more – after all this Adobe might get a little profit – then there’s taxes… Which leads me on.

Adobe ask for around 25K for an enterprise license of LiveCycle, which, like ColdFusion is a Java based app that generates business documents (no, really) – Yet they can only ask around 4K for a ColdFusion license – why would this be so if Adobe hadn’t realised that about 4K is all they are going to get before they start scaring developers off? So it leaves them between a rock and a hard place.

Given that ColdFusion is pretty well established as an app server, in that it is relatively bug free and reasonably reliable – do they even need to continue development, or just shelve it and use it to push their *other* front end products? I would be leaning toward the second option.

Which leads me to the next point. In my mind, ColdFusion is “old hat” – while it was excellent in the 90’s and pretty good for the first half of this century, it’s becoming out-competed by the likes of .net and Java – not due to ease of development nor performance, but simply because more people out there know Java and .net – these languages are taught widely and are more trusted, simply due to the reputation of their respective vendors.

As said earlier, Adobe are between a rock and a hard place – and this is supply and demand in action – if they made it cheaper, more people would use it. However, then they would need to increase support and invest in their reputation as an application services vendor and for that they would need money, hence they would need to increase prices and effectively kill demand.