Creating an URL Shortener with ColdFusion

There are several sites to use for URL shortening, like tinyUrl.com, is.gd, dwarfURL.com, bit.ly and others.
I got a call from a friend who has many articles distributed with short URL and he is afraid if someday those shortening sites go down and his articles will be lost.
Therefore, he asked me if I could write a system to do the same thing but hosted on his server.
I did it and it is working for him. I now decided to make it available for those who think the same way.

Our environment is Windows Server 2003, Microsoft SQL 2008 and ColdFusion 8.

Domain Name

First, you need to register a short domain name and create the website. You can try free domain hosting for that matter. Suppose you registered the domain xxx.com, and set up the DNS records.

  • Create a folder for the site in your web root: ex: c:\inetpub\wwwroot\xxx\
    • Add it to IIS

ISAPI Rewrite

In order to redirect the short URL to the target one, you need to set up the ISAPI URL rewrite.

  • If you have IIS 5/6:
    • You should download the DLL and the configuration file HERE.
    • Save the IsapiRewrite4.dll and the configuration file (.ini) to a folder outside your web root. I saved it into “c:\ISAPI\urlShortener\”.
      The configuration file is simple, and you will see the following rule:
      RewriteRule  ^/([^/.\?]+)$    /index.cfm?go=$1
  • For IIS 7 (Windows Server 2008 or Windows 7):
    • You need to download the ISAPI rewrite tool (http://www.iis.net/download/URLRewrite)
      • Click on the link x86 or x64 (64-bit) on the box on the right, if you do not want to install the Microsoft Web Platform Installer.
    • Run the installation and your IIS will have the “Rewrite” tool for all of your sites.
    • Configure ISAPI:
      • Open the IIS Manager and select your website or virtual folder (Windows 7 localhost)
      • In the IIS group icons, click on “URL rewrite”
      • The URL Rewrite window will open, click on “Add Rule(s)” on the right box.
      • On the Rule Template, select “User-friendly URL” and click OK.
      • Enter the following URL in the input box: http://xxx.com/index.cfm?go=xyz123
        (replace xxx.com by your domain).

        • IIS will show the short URL and the pattern in the boxes below. Just click OK.
      • Now the URL Rewrite window will show the rule you just created.

Database

Next step: Setting up the database:

  • Create a database and a table (I called the database “urlShortener” and the table “urls”).  (Scripts HERE). (codes SP1 and SP2)
  • Create two stored procedures: dbo.setShortUrl and dbo.getShortUrl  (Scripts HERE).

o   The stored procedure setShortUrl checks if the target URL and code passed already Exist, and if not, it saves the new target and returns the new code. (code SP3)

o   The stored procedure getShortUrl retrieves the target URL of a given short code.(code SP4)

ColdFusion Data Source

Add data source to ColdFusion Administrator: I suppose you know how to do that!

CFML

Now let’s work on the project in CFEclipse or CFBuilder.

  • Create the “Application.cfc” (Code CF1)
  • Create the component “shortener.cfc” (code CF2)
  • Create the “index.cfm” (code CF3)

You may download the complete code and database scripts HERE.

I hope this code is of good use for you. Please let me know if you find any error or if you have any ideas to make it work better.

Code S1: Create Database urlShortener


USE [master]
GO
CREATE DATABASE [urlShortener] ON  PRIMARY
( NAME = N'urlShortener', FILENAME = N'C:\MSSQLDATA\urlShortener.mdf' , SIZE = 2048KB , MAXSIZE = UNLIMITED, FILEGROWTH = 1024KB )

LOG ON
( NAME = N'urlShortener_log', FILENAME = N'C:\MSSQLDATA\urlShortener_log.ldf' , SIZE = 1024KB , MAXSIZE = 2048GB , FILEGROWTH = 2048KB )
GO

ALTER DATABASE [urlShortener] SET COMPATIBILITY_LEVEL = 100
GO

IF (1 = FULLTEXTSERVICEPROPERTY('IsFullTextInstalled'))
begin
EXEC [urlShortener].[dbo].[sp_fulltext_database] @action = 'enable'
end
GO

ALTER DATABASE [urlShortener] SET ANSI_NULL_DEFAULT OFF
GO

ALTER DATABASE [urlShortener] SET ANSI_NULLS OFF
GO

ALTER DATABASE [urlShortener] SET ANSI_PADDING OFF
GO

ALTER DATABASE [urlShortener] SET ANSI_WARNINGS OFF
GO

ALTER DATABASE [urlShortener] SET ARITHABORT OFF
GO

ALTER DATABASE [urlShortener] SET AUTO_CLOSE OFF
GO

ALTER DATABASE [urlShortener] SET AUTO_CREATE_STATISTICS ON
GO

ALTER DATABASE [urlShortener] SET AUTO_SHRINK OFF
GO

ALTER DATABASE [urlShortener] SET AUTO_UPDATE_STATISTICS ON
GO

ALTER DATABASE [urlShortener] SET CURSOR_CLOSE_ON_COMMIT OFF
GO

ALTER DATABASE [urlShortener] SET CURSOR_DEFAULT  GLOBAL
GO

ALTER DATABASE [urlShortener] SET CONCAT_NULL_YIELDS_NULL OFF
GO

ALTER DATABASE [urlShortener] SET NUMERIC_ROUNDABORT OFF
GO

ALTER DATABASE [urlShortener] SET QUOTED_IDENTIFIER OFF
GO

ALTER DATABASE [urlShortener] SET RECURSIVE_TRIGGERS OFF
GO

ALTER DATABASE [urlShortener] SET  DISABLE_BROKER
GO

ALTER DATABASE [urlShortener] SET AUTO_UPDATE_STATISTICS_ASYNC OFF
GO

ALTER DATABASE [urlShortener] SET DATE_CORRELATION_OPTIMIZATION OFF
GO

ALTER DATABASE [urlShortener] SET TRUSTWORTHY OFF
GO

ALTER DATABASE [urlShortener] SET ALLOW_SNAPSHOT_ISOLATION OFF
GO

ALTER DATABASE [urlShortener] SET PARAMETERIZATION SIMPLE
GO

ALTER DATABASE [urlShortener] SET READ_COMMITTED_SNAPSHOT OFF
GO

ALTER DATABASE [urlShortener] SET HONOR_BROKER_PRIORITY OFF
GO

ALTER DATABASE [urlShortener] SET  READ_WRITE
GO

ALTER DATABASE [urlShortener] SET RECOVERY SIMPLE
GO

ALTER DATABASE [urlShortener] SET  MULTI_USER
GO

ALTER DATABASE [urlShortener] SET PAGE_VERIFY CHECKSUM
GO

ALTER DATABASE [urlShortener] SET DB_CHAINING OFF
GO

Code S2: Create Table urls


USE [urlShortener]
GO

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

SET ANSI_PADDING ON
GO

CREATE TABLE [dbo].[urls](
[ID] [uniqueidentifier] NOT NULL,
[shortURL] [varchar](25) NULL,
[targetURL] [varchar](255) NULL,
[hitCount] [int] NULL,
[dateCreated] [datetime] NULL,
[isActive] [bit] NULL,

CONSTRAINT [PK_urls] 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]
GO

SET ANSI_PADDING OFF
GO

ALTER TABLE [dbo].[urls] ADD  CONSTRAINT [DF_urls_ID]  DEFAULT (newid()) FOR [ID]
GO

ALTER TABLE [dbo].[urls] ADD  CONSTRAINT [DF_urls_hitCount]  DEFAULT ((0)) FOR [hitCount]
GO

ALTER TABLE [dbo].[urls] ADD  CONSTRAINT [DF_urls_dateCreated]  DEFAULT (getdate()) FOR [dateCreated]
GO

ALTER TABLE [dbo].[urls] ADD  CONSTRAINT [DF_urls_isActive]  DEFAULT ((1)) FOR [isActive]
GO

Code S3: Stored Procedure setShortURL


USE [urlShortener]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author:           Ricardo Parente
-- Create date: 2010-10-09
-- Description:      I try to add a new URL
-- =============================================
CREATE PROCEDURE [dbo].[setShortUrl]
@arg_shortURL varchar(25)
,   @arg_targetURL varchar(255)
AS
BEGIN
SET NOCOUNT ON;
-- check if targetURL already exists
IF (NOT EXISTS (
SELECT TOP 1 shortURL
FROM urls
WHERE targetURL = @arg_targetURL))
BEGIN
-- since targetURL does not exist, let's check if the
-- shortURL already exists
IF (NOT EXISTS (
SELECT TOP 1 shortURL
FROM urls
WHERE shortURL = @arg_shortURL))
BEGIN
-- since the shortURL and targetURL do not exist, let's add them
INSERT INTO urls (
shortURL
,   targetURL
) VALUES (
@arg_shortURL
,   @arg_targetURL
)
-- return the newly created shortURL
SELECT @arg_shortURL AS shortURL
END
ELSE
-- shortURL already exists and it is not for the given target URL, so return blank
SELECT NULL AS shortURL
END
ELSE
-- targetURL already exists, so return its shortURL
SELECT TOP 1 shortURL
FROM urls
WHERE targetURL = @arg_targetURL
END
GO

<h2>Code S4: Stored Procedure getShortURL</h2>


USE [urlShortener]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author:        Ricardo Parente
-- Create date: 2010-10-10
-- Description:   I retrieve one record
-- =============================================
CREATE PROCEDURE [dbo].[getShortURL]
@arg_shortURL varchar(25)
AS
BEGIN
SET NOCOUNT ON;
UPDATE urls SET
hitCount = hitCount + 1
WHERE shortURL = @arg_shortURL
SELECT TOP 1 targetURL
FROM urls
WHERE shortURL = @arg_shortURL
END

Code CF1: Aplication.cfc


<cfcomponent output="false">
<cfscript>
   this.name = "urlShortener";
   this.applicationTimeout = createTimeSpan(0,6,0,0);
   this.sessionManagement = false;
   this.clientManagement = false;
</cfscript>
<cffunction name="onApplicationStart" returntype="boolean" output="false">
   <cfscript>
      application.mainDSN = "urlShortener";
      application.emailFrom = "process@zzz.com";
      application.emailAdmin = "webmaster@zzz.com";
      application.totChars = 6;
   </cfscript>
   <cfreturn True/>
</cffunction>

<cffunction name="onApplicationEnd" output="false">
   <cfargument name="applicationScope" required="true" />
</cffunction>

<cffunction name="onRequestStart">
   <cfargument type="String" name="targetPage" required="true" />
   <cfparam name="url.appInit" type="string" default="false" />
   <cfparam name="url.serverInit" type="string" default="false" />
   <cfscript>
      if (url.appInit eq true or url.serverInit eq true)
         onApplicationStart();
   </cfscript>
</cffunction>
</cfcomponent>

Code CF2: shortener.cfc


<cfcomponent output="false">
<cfscript>
   variables.aChars =
      listToArray("a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 7 8 9", " ");
</cfscript>

<cffunction name="setShort" access="remote" output="false" returntype="Any">
   <cfargument name="targetURL" type="string" required="true" />
   <cfset var myShort = "" />
   <cfset var totChars = application.totChars />
   <cfset var shortURL = "" />
   <!--- try up to 10 times to avoid duplicate shortURL --->
   <cfloop from="1" to="10" index="x">
      <cfset myShort = createShort() />
      <cfset shortURL = saveShort(myShort,arguments.targetURL) />
      <cfif len(trim(shortURL))>
         <cfbreak />
      </cfif>
   </cfloop>
   <cfif not len(trim(shortURL))>
      <!--- try again with more characters --->
      <cfset totChars = totChars + 1 />
      <cfloop from="1" to="10" index="x">
         <cfset myShort = createShort(totChars) />
         <cfset shortURL = saveShort(myShort,arguments.targetURL) />
         <cfif len(trim(shortURL))>
            <cfbreak />
         </cfif>
      </cfloop>
   </cfif>
   <cfreturn shortURL />
</cffunction>

<cffunction name="saveShort" access="private" output="false" returntype="Any">
   <cfargument name="shortURL" type="string" required="true" />
   <cfargument name="targetURL" type="string" required="true" />
   <cfset var myShortURL = "" />
   <cftry>
      <!--- this procedure will save the url and return the shorURL generated --->
      <cfstoredproc procedure="dbo.setShortURL" datasource="#application.mainDSN#">
         <cfprocparam cfsqltype="CF_SQL_VARCHAR" value="#arguments.shortURL#" />
         <cfprocparam cfsqltype="CF_SQL_VARCHAR" value="#arguments.targetURL#" />
         <cfprocresult name="qShort" />
      </cfstoredproc>
      <cfset myShortURL = qShort.shortURL />
      <cfcatch>
         <cfmail from="#application.emailFrom#" To="#application.emailAdmin#" subject="Error on SetShort function" Type="html">
            <h1>Error trying to set short URL</h1>
               <cfdump var="#cfcatch#">
         </cfmail>
         <cfset myShortURL = "" />
      </cfcatch>
   </cftry>
   <cfreturn myShortURL />
</cffunction>

<cffunction name="createShort" access="private" output="false" returntype="Any">
   <cfargument name="totChars" type="numeric" required="false" default="#application.totChars#" />
   <cfscript>
      var shortUrl = "";
      for (i=1; i lte arguments.totChars; i=i+1) {
         shortUrl = shortUrl &amp;amp; variables.aChars[randRange(1,arrayLen(variables.aChars))];
      }
      return shortUrl;
   </cfscript>
</cffunction>

<!--- function set to remote to be called as webservice --->
<cffunction name="getShortURL" access="remote" output="false" returntype="Any">
   <cfargument name="targetURL" type="string" required="true" />
   <cfreturn setShort(arguments.targetURL) /></cffunction>
</cfcomponent>

<h2>Code CF3: index.cfm</h2>

<cfparam name="url.site" type="string" default="" />
<cfparam name="url.go" type="string" default="" />
<!--- let's check if we are in development server or localhost --->
<cfif listFindNoCase("localhost,127.0.0.1", cgi.host_name)>
   <cfset baseHost = "http://localhost/" />
<cfelse>
   <cfset baseHost = "http://lk3.us/" />
</cfif>
<!--- if a target URL was passed, we need to create the short URL --->
<cfif len(trim(url.site))>
   <cfset short = createObject("component", "shorten") />
   <cfif left(url.site, 7) neq 'http://'>
      <cfset url.site = "http://" &amp;amp; url.site />
   </cfif>
   <cfset shortURL = short.setShort(url.site) />
   <cfif len(trim(shortURL))>
      <cfset shortURL = baseHost &amp;amp; shortURL />
      <cfoutput>
      The short URL for: #url.site# is: <br/>
      <a href="#shortURL#">#shortURL#</a>
      </cfoutput>
      <!--- if any error occurred during the creation of the short URL --->
   <cfelse>
      <cfoutput>
      Sorry! We are experiencing problems now. Please try again later !
      </cfoutput>
   </cfif>
   <cfabort />
<!--- check if a short URL was passed, then get the target and jump to it --->
<cfelseif len(trim(url.go))>
   <cftry>
      <cfstoredproc procedure="dbo.getShortURL" datasource="#application.mainDSN#">
         <cfprocparam cfsqltype="CF_SQL_VARCHAR" value="#url.go#" />
         <cfprocresult name="get" />
      </cfstoredproc>
      <cfcatch>
         <cfoutput>
         Sorry ! Short URL unavailable !<br/>
         #cfcatch.message#<br/>
         #cfcatch.detail#
         </cfoutput>
         <cfabort />
      </cfcatch>
   </cftry>
   <!--- jump to the target --->
   <cfheader statuscode="301" statustext="Moved Permanently" />
   <cfheader name="Location" value="#get.targetURL#" />
<cfelse>
   Please verify the query string. It should have either "site" or "go" parameters.
</cfif>

One thought on “Creating an URL Shortener with ColdFusion

Leave a Reply