News, tips, tricks and discussions related to Modern Signal Lighthouse, ColdFusion, ASP.NET and other web development technologies.
In a previous post, I described my experience working with Facebook Login integration. I recently implemented Google login integration (aka Google+ Sign-In), which is very similar to Facebook's integration. This feature enables Google users to login to your site using their Google credentials. As such, the user does not need to create and remember a username and password for your site. Their Google credentials are used instead.
Here is the main page on Google's site regarding Google+ Sign-In: https://developers.google.com/+/web/signin/. The majority of the information is very straight-forward. However, I will share a few pieces of code that pulled it all together for me.
Here is my signinCallback function code, which redirects the browser window upon successful Google login. Note that the accessToken parameter is sent to our server for further processing.
function signinCallback(authResult) {
if (authResult['access_token']) {
// Successfully authorized
window.location =
"/MyAccount/GoogleLogOn?returnURL=@(Request["returnURL"])&accessToken="
+ authResult['access_token'];
}
}
Here is my server-side C# code that uses the accessToken provided by Google to obtain the associated Google UserID.
string googleUserID = "";
using (var webClient = new System.Net.WebClient())
{
var json = webClient.DownloadString("https://www.googleapis.com/plus/v1/people/me?access_token=" + accessToken);
JObject o = JObject.Parse(json);
googleUserID = (string)o["id"];
}
Once we have the associated Google UserID, we can poll our database and confirm that a user exists in it with this associated Google UserID. If so, we can log the user in.
Comments (0) Posted on March 11, 2013 4:43:46 PM EDT by Adam Polon
In November 2012, Modern Signal launched a redesign of our site. We were (are!) very proud of how it looked and the ways it would allow us to keep future and current clients better informed. One problem: it wasn't responsive. Our redesign had been in the works for a while, and the design had been approved and finalized before we became fully aware of a new reality: because of the proliferation of smartphones and tablets, modern websites must adjust to a wide variety of screen sizes. They must be responsive.

The problem was that we had just invested considerable time and energy in redesigning our site. We were happy with the way it looked on desktop browsers, and we didn't want to start over. The question arose, can we keep our design and just make it responsive? This same question has started to come up with many of our clients too, and so it was definitely worth investigating.
If you ask a designer whether you need to start from scratch to make your site responsive, they are likely to say, more or less, yes. That is, you may be able to keep much of the overall look and feel of your site, but you'll have to rethink so many things you are better off starting over. I'm not a designer, though. I'm a developer with years of experience in the trenches with html, css, and javascript. And so in my usual naivete I thought, "How hard could it be?"
"How Hard Could It Be?"
The answer to this question is, of course, "It depends." The designers are right in that you have to rethink many of the visual elements of your site in order to make it responsive. You likely have a top navigation bar with drop-down menus. You likely have a side navigation bar which will not fit on smaller screens. You may have some fixed-width background images that are not going to expand and contract the way they need to in a responsive site. The Modern Signal site had all of those things.
In the end, though, all of these challenges were surmountable with css, elbow grease, and just a little bit of targeted redesign. I'll take you through how we tackled these challenges.
"What Needs to Change?"
The first step in making any existing design responsive is to walk through the site to identify things that have to change. Mainly you are looking for:
There are other less well-defined things you might want to consider, like how much content people will comfortably scroll through on a phone, but these are the biggest red flags. In our site this list brought up:
So, knowing what I needed to attack, I dove in.
BootStrap - Laying the Groundwork
First things first. We had become familiar with Twitter's Bootstrap framework. We like it for its attractive set of widgets as well as it's framework for building responsive sites. The first step, then, was to integrate the Bootstrap CSS files and change the html of the site to use the Bootstrap concepts of "rows" and "spans". I won't delve into this too much as I doubt there is much I can add to the information already available.
"How to Go from Fixed-width to Expandable?"
Our right navigation presented an immediate problem: it used fixed-width background images in order to give it a distinctive banner look and feel:
The html and css looked very like this (only including relevant css):
html:
<h3 class="subnav-section">
<a href="/aboutus">About Us</a>
</h3>
css:
.subnav .subnav-section a {
background: url('../images/subnav-section-bkg.png') 0 0 no-repeat;
}
This is simple, effective and ... not responsive. In the end, I ended up coming to terms with an ugly fact: I couldn't fix this with css alone. At least not with the css available to me (maybe I can ignore IE6, maybe even IE7, but not IE8!) I would have to add a little html to make it work. What I ended up with was:
html:
<div class="banner-header">
<h3>
<a href="/about_us">About Us</a>
</h3>
</div>
css:
.banner-header {
background: url('../images/banner-bkg-left.png') left 0 no-repeat;
padding-left: 30px;
}
.banner-header h3 {
background: url('../images/banner-bkg-right.png') right 0 no-repeat;
padding-right: 30px;
}
I added an extra div tag around the header. The background of the div tag is the left side of the banner, while the background of the h3 tag is the right side of the banner. The middle of the banner is, mercifully, just a plain black background and can expand to its heart's content. This worked well, and only forced me to add one html tag.
On to the next challenge...
"How to Unclutter the Top Navigation?"
Our top navigation is a relatively simple line of links with drop-down menus that appear when you hover over the links. They wrap fine, but get pretty jumbled on small, phone-size screens. They also use hover actions to display submenus, which doesn't work on devices that use touch screens.
Here is a case where simply rearranging the existing design elements didn't work. We needed to rethink the design a bit. The default Bootstrap template deals with this by transforming the navigation into a single button that displays a drop-down of the entire menu when clicked. We ended up using the same idea:
What's important to note about this is that we didn't need to rethink the design of our entire page to accomplish this -- we just had to rethink this one element.
"What About the Carousel?"
Our next challenge was dealing with the carousel on our home page. Large home page carousels are very popular on websites nowadays. They're splashy and can be used to draw the visitor's attention to news, products, featured services, etc. They usually, however, take up the full width of the page and so present a problem for responsive design.
So, it's a common problem, which is fortunate. I quickly found Flexslider, which handled most of the responsive challenges for me. It took very little time for me to replace our current carousel code with Flexslider.
We still had a problem, though. The items in our carousel contain images and text. We can make the images progressively smaller, but at some point we just don't have enough room to make the text fit over the images without making the text too small to see. The answer for us was to hide things progressively as the page width gets smaller. This is very easy to do using some built-in Bootstrap classes. If you want something to be hidden on tablet-sized devices or smaller, use the "hidden-tablet" class:
<div class="hidden-table">...</div>
If you want something to be hidden on phone-sized devices or smaller, use the "hidden-tablet" class:
<div class="hidden-phone">...</div>
Using these classes, we hide the descriptive text in the carousal on devices smaller than 1024px wide, and we hide the carousel altogether on devices smaller than 768px. So go from:
on large devices to:
on medium-sized devices to:
on very small devices.
Conclusions
In the end, we're very happy with the results of the responsive web design overhaul of our site. We got it done much faster and cheaper than we would have had we gone back to the design phase. Of course, if we had been in the market for a full redesign anyway, we would have done that, but we were happy with our design and just wanted to add some responsive sugar to it, to in effect make it a mobile website and a tablet website as well as one that looks good on the desktop. We have heard from a lot of people who are in the same boat, and I hope this post gives heart to those who would like a responsive site but are not ready for a redesign.
Interested in implementing responsive elements on your own site? Download this checklist of things you should keep in mind.
Comments (0) Posted on February 25, 2013 12:19:48 PM EST by Sally Byrd
I recently worked on integrating "Login with Facebook" functionality into our Lighthouse.Net CMS. This feature enables users to login to a third-party website using their Facebook credentials. As such, the user does not need to create and remember a username and password for your site. Their Facebook credentials are used instead.
While I found a lot of information online about this integration, I was unable to find a great resource that fully explained how to get from the user successfully logging in using their Facebook credentials (which occurs in the browser using Facebook's Client-Side Javascript SDK) to securely logging the user into our site. Here are the questions I had and how I addressed them:
Question 1:
What data do we need to store in our Users database table to support Facebook integration?
Answer 1:
We added a column for FacebookID. This is an integer value generated by Facebook that serves as the primary key for each user.
Question 2:
When a user logs in using the "Login with Facebook" button (which happens in the browser using the Javascript SDK), what data do we need to pass from the browser to our server to log the user in to our app.
Answer 2:
When a user successfully logs in using Facebook's Client-Side Javascript SDK, an OAuth 2 access token is exposed to your Javascript code (accessible as response.authResponse.accessToken). This token is a temporary token which can be used to retreive data for this user using Facebook's Social Graph API. Here is the login flow our software follows:
This logic relies on the fact that the access token generated by Facebook is complex, temporary, and unique. If an access code can successfully be used to retrieve a FacebookID from the Social Graph, we can trust that the FacebookID we receive back is associated with correct user. This takes the place of a user entering their username and password into a web form.
Here is the HTML and Javascript code required to add the Login with Facebook button to your site.
<script src=https://connect.facebook.net/en_US/all.js type="text/javascript"></script>
<script>
$("document").ready(function () {
// Initialize the SDK upon load
FB.init({
appId: 'YOUR_FACEBOOK_APPID', // App ID
channelUrl: '', // Path to your Channel File
scope: '', // This to get the user details back from Facebook
status: true, // check login status
cookie: true, // enable cookies to allow the server to access the session
xfbml: true // parse XFBML
});
// listen for and handle auth.statusChange events
FB.Event.subscribe('auth.login', OnLogin);
});
// This method will be called after the user login into facebook.
function OnLogin(response) {
if (response.authResponse) {
window.location = "/FacebookLogOn?accessToken=" + response.authResponse.accessToken;
}
}
</script>
<div id="fb-root"></div> <!-- This initializes the FB controls-->
<div class="fb-login-button" autologoutlink="true" scope="" >Login with Facebook</div> <!-- FB Login Button -->
<br /><br />
Comments (0) Posted on November 28, 2012 10:30:41 AM EST by Adam Polon
I was recently looking for a way to throttle the number of requests to a site from bad bots that sometimes flood a server with dozens of requests in a few seconds. I found a very nice simple function by Charlie Arehart:
http://www.carehart.org/blog/client/index.cfm/2010/5/21/throttling_by_ip_address
Before proceeding, you might want to look over Charlie's article, which nicely lays out many of the considerations and caveats that come with limiting the number of requests by IP address.
Aside from all the caveats, Charlie's function had a couple things I wanted to adjust. One was that, as written, it would block even "good" bots, like Googlebot, if they made requests more frequently than the "duration" parameter. For instance, if you set it up to allow up to 6 requests every 3 seconds (as I did), it would end up blocking Googlebot from making a steady stream of requests at the frequency of once per second. So even though Googlebot was only making 3 requests every 3 seconds it was getting blocked.
Adjusting Charlie's function to stop doing that was very easy. But the other thing I was concerned about was whether keeping a list of IP addresses in an application variable would scale very well. How long would the list have to be before it would affect performance? I didn't really know, but felt it would be better to have a solution where the list would have old IP addresses pruned periodically. I toyed with the idea of using a scheduled task to go through the list and removed old entries. That seemed cumbersome, though. In the end, since the site I was working on was running ColdFusion 9, I thought it would be a good opportunity to use ColdFusion 9's new caching functions. Using ColdFusion's built-in caching functions means the I can have old IP addresses cleaned up automatically.
So here is my version of the rate limiter. This is generally more forgiving than Charlie's version of the function. In Charlie's version if a bot is blocked but continues to make requests (e.g. Googlebot), it will continue to be blocked until it pauses for the "duration". In my version, the bot is blocked for the "duration" and then allowed to make more requests before being blocked again. My main goal is to block the rogue bots that flood the system with 10 or 20 requests a second for a short time, and this does a pretty good job of that.
<cffunction name="limiter">
<cfargument name="duration" type="numeric" default=3>
<cfargument name="count" type="numeric" default=6>
<cfset var cacheId = "rate_limiter_" & CGI.REMOTE_ADDR>
<cfset var rate = cacheGet(cacheId)>
<cfif isNull(rate)>
<!--- Create cached object --->
<cfset cachePut(cacheID, {attempts = 1, start = Now()}, createTimeSpan(0,0,1,0))>
<cfelseif DateDiff("s", rate.start, Now()) LT arguments.duration>
<cfif rate.attempts gte arguments.count>
<cfoutput>
<p>You are making too many requests too fast,
please slow down and wait #arguments.duration# seconds</p>
</cfoutput>
<cfheader statuscode="503" statustext="Service Unavailable">
<cfheader name="Retry-After" value="#arguments.duration#">
<cflog file="limiter" text="#cgi.remote_addr# #rate.attempts# #cgi.request_method# #cgi.SCRIPT_NAME# #cgi.QUERY_STRING# #cgi.http_user_agent# #rate.start#">
<cfif rate.attempts is arguments.count>
<!--- Lock out for duration --->
<cfset cachePut(cacheID, {attempts = rate.attempts + 1, start = Now()}, createTimeSpan(0,0,1,0))>
</cfif>
<cfabort>
<cfelse>
<!--- Increment attempts --->
<cfset cachePut(cacheID, {attempts = rate.attempts + 1, start = rate.start}, createTimeSpan(0,0,1,0))>
</cfif>
<cfelse>
<!--- Reset attempts --->
<cfset cachePut(cacheID, {attempts = 1, start = Now()}, createTimeSpan(0,0,1,0))>
</cfif>
</cffunction>
Comments (4) Posted on April 2, 2012 2:58:45 PM EDT by Catherine Field
I wanted to write about another bit of code that I created for the What Color Is...? website I talked about in my previous post.
Displaying the Results
Probably the most interesting and challenging part of this project was to come up with a way to generate and display the results in an efficient way. Colors were all saved in the database as 3 separate fields (Red, Green, and Blue). This made it easy to select a color average for all responses. Here is the SQL:
SELECT t.name ,
COUNT(*) AS Count,
AVG(IsNull(red,255)) as Red ,
AVG(IsNull(green,255)) as Green ,
AVG(IsNull(blue,255)) as Blue
FROM WCIResponse r
INNER JOIN WCIThing t ON r.ThingID = t.ThingID
GROUP BY t.name
One of the options in the survey is to select "No Color", which is saved in the database as null values. These are converted to 255 so that it is the same as if they selected white.
That was pretty easy, but I also collect some basic, anonymous demographic information to enable breaking down the results a bit. I ask for age, for instance. What I wanted to do was to break down the results by age group, i.e. 20-29, 30-39, etc. This is more complicated, of course, and I didn't want to get into doing a massive number of queries and loops, etc, to build up the results. Fortunately I have recently gotten to know more about the SQL Server statement PIVOT. PIVOT is made for creating just such reports as this. This is somewhat more complicated than a simple PIVOT, because I don't want a column for every age, but I want to group the ages by decade.
I'll run through the ColdFusion/SQL code I used to make this work.
First, I set a variable with an SQL expression to create the name of the age group, e.g. "40-49":
<cfset var AgeRange = "CONVERT(VARCHAR,Age/10*10) + '-' + CONVERT(VARCHAR,Age/10*10+9)">
I only want to include the age ranges for which I actually have data, so I do an initial query to get the age ranges in the data. Note that the AgeRange is wrapped in brackets so that it can be used as the column name in the next query, e.g. "[40-49]".
<cfquery name="GetAges" datasource="#application.dsn#">
SELECT DISTINCT
'[' + #preserveSingleQuotes(AgeRange)# + ']' AS AgeRange
FROM WCIPerson
WHERE Age IS NOT NULL
ORDER BY AgeRange
</cfquery>
I can then build the PIVOT query including all of the age ranges. I'm still not comfortable with the PIVOT syntax. I've only used it a few times, and it's still hard for me to wrap my head around without looking at documentation and examples. If you're new to PIVOT too, this site has a few good examples. As you can see below, I reuse the AgeRange variable I set above twice.
<cfquery name="GetResults" datasource="#application.dsn#">
SELECT t2.name
<cfloop query="GetAges">
,ISNULL(t2.#GetAges.AgeRange#,'0,255,255,255') AS #GetAges.AgeRange#
</cfloop>
FROM ( SELECT t.ThingID ,
t.name ,
#preserveSingleQuotes(AgeRange)# AS AgeRange,
CONVERT(VARCHAR, COUNT(*)) + ','
+ CONVERT(VARCHAR, AVG(ISNULL(red, 255))) + ','
+ CONVERT(VARCHAR, AVG(ISNULL(green, 255))) + ','
+ CONVERT(VARCHAR, AVG(ISNULL(blue, 255))) AS Color
FROM WCIResponse r
INNER JOIN WCIThing t ON r.ThingID = t.ThingID
INNER JOIN WCIPerson p ON r.PersonID = p.PersonID
WHERE p.Age IS NOT NULL
GROUP BY t.ThingID ,
t.name ,
#preserveSingleQuotes(AgeRange)#
) AS t PIVOT ( MAX(color) FOR AgeRange IN ( #ValueList(GetAges.AgeRange)# ) ) AS t2
</cfquery>
Although the What Color Is...? site doesn't have a lot of data at this point, I believe this code should scale pretty well. Of course in the unlikely event that the site becomes an internet phenomenon and gets millions of responses I may have to revisit this code.
Comments (0) Posted on January 31, 2011 5:09:23 PM EST by Catherine Field
I recently created a fun little weekend project (well, an MLK Day project to be more specific). It's a simple survey to query users about what colors come to mind when they think about abstract ideas. It's called What Color Is...? The original motivation behind the site was to come up with an idea for my daughter's science fair project. She sees letters and numbers as colors (something called grapheme-color synesthesia). I came up with the idea to devise a simple test to see what colors, if any, most people see when they think about an abstract idea. From there I naturally began thinking about how it could be turned into a slick online survey. It was an interesting little project that had a number of minor technical challenges along the way.
A Touch of Eye-candy: Colorify jQuery Plugin
For the design of the site, I chose a basic gray jQuery UI theme
(Smoothness). I did this so as to not distract from the main point of
the survey by putting too much color into the surrounding UI. I did
feel like adding a little bit of fun color into the UI though. I
decided that when I would put in some multicolored text in the page
titles. To achieve this, I ended up creating a simple jQuery plugin
that takes any element and assigns a string of random colors to the
letters in the text. I called it "colorify":
(function($){
$.fn.colorify = function(){
var cify = function(s){
var rgb = function(){
var i = Math.floor(Math.random() * 3);
var r = function(n){
return n == i ? 0 : Math.floor(Math.random() * 256);
}
return "rgb(" + r(0) + "," + r(1) + "," + r(2) + ")";
}
var s2 = "";
for (var i = 0; i < s.length; i++) {
s2 += "<span style=\"color:" + rgb() + ";\">" + s.substr(i, 1) + "</span>";
}
return s2;
}
this.html(cify(this.html()));
};
})(jQuery);
This goes through each letter in the text, gets a random RGB
value and assigns it to the letter by wrapping a span tag with a style
parameter around the letter. It's used like any simple jQuery plugin:
$("#wcititle").colorify();
Which creates colored text like this:
What Color Is...?
One interesting thing about this is that my first attempt at the function resulted in some colors that were just too light to show up well on a white background. That's an obvious problem when getting completely random colors. The solution I found was to randomly choose one of the 3 color components (R, G, or B) and set that to 0 (full saturation). This preserves the generation of bright colors without making them too light.
I wouldn't call this a very useful plugin -- I'll probably never use it for another project. But it was kind of fun to write. Check it out in action at What Color Is...?
Comments (0) Posted on January 31, 2011 4:44:37 PM EST by Catherine Field
I just implemented something that should have been simple, and actually is simple, but which ended up being a bit more of a struggle than it should have been.
We have a site where occasionally we were getting duplicate payment transactions when people submitted orders. The transactions were all within a couple seconds of each other, so it appeared that people were just double-clicking the submit button (despite, of course, a message on the page saying to only click the button once). We did, once upon a time, have javascript code in place that disabled the submit button when it was clicked once, but that code has been removed somewhere along the line. I think it had been disabled due to the difficulty of dealing with client side validation -- i.e. if the client-side validation failed, the button should not be disabled, because then the user will not be able to submit the form.
I found a variety of posts online about dealing with this problem. The basic solution is simple, which is to insert code into the form submit handler that runs the client-side validation and only disables the button if it passes. First I set up this javascript function:
<script type="text/javascript">
function handleSubmit() {
if (typeof (ValidatorOnSubmit) == 'function' && ValidatorOnSubmit() == false) {
return false;
} else {
$("form#aspnetForm input[type=submit]")
.click(function() { return false })
.fadeTo(200, 0.5);
return true;
}
}
</script>
Then in the page's code behind, I registered the onsubmit function like so:
Page.ClientScript.RegisterOnSubmitStatement(Me.GetType, "OnSubmitScript", "return handleSubmit()")
The javascript function runs the same code that ASP.Net automatically runs for client-side validation, and, if the validation passes, uses jQuery to disable the submit button and fade its appearance. The fading isn't necessary, but since it's so easy to do with jQuery, why not?
There is one thing in there though, that threw me for a loop. Most of the posts I found online set the disabled parameter for the button, which you could do with jQuery like this:
$("form#aspnetForm input[type=submit]").attr("disabled", "disabled");
But when I did that I found that the submit action tied to the button no longer fired on the server side. After puzzling over this for a while, I realized that when the button is disabled, the button value is no longer sent along with the form parameters, and thus the server-size click event for the button is not fired. I wasn't able to find any references to this problem online (hence the impetus for this blog post). So instead of using the disabled parameter, I set the click event of the button to return false, which seems to work well.
Something else that I like about this solution, is that it is very generic. Since I use a generic selector to get the submit button ("form#aspnetForm input[type=submit]"), the javascript function could be included in a site-wide javascript library, and the onsubmit function could be registered in a master page to enable this functionality for all submit buttons on a site. I'm not ready to do that on this site, since I just want to make sure the order checkout works well for now, but it's nice to know that if this comes up again, I can fix it in a flash.
Comments (2) Posted on November 5, 2010 3:22:46 PM EDT by Catherine Field
I wanted to take a few minutes to talk up a cool little tool that I started using a few weeks ago and have since come to rely upon.
smtp4dev (http://smtp4dev.codeplex.com/) is a dummy smtp server that, instead of sending email messages, stores them in a list for easy viewing and inspection. Before I started using this tool, any time I wanted to test email sending for an application I had to, first of all, sanitize all of my data to make sure I didn't accidentally send messages to people that shouldn't get them, and then make sure any messages I needed to inspect were sent to my email account. I would have to wait for messages to arrive in my inbox and deal with all of the test messages getting mixed up with my regular mail. It was a big hassle, in other words.
An added benefit for me is that, since I access my regular mail through Gmail, I am able to easily open up the messages in Windows Live Mail (also a free download) which seems to replicate the way the email displays in Outlook. For highly styled messages this is very important, since Outlook is a very popular client that has quite a few display quirks.
Comments (0) Posted on July 29, 2010 9:45:06 AM EDT by Catherine Field
We had a client trying to upload a 44mb file, and it was failing. The ColdFusion page that that did the upload had a very high timeout setting, so I didn't think that timeout was the issue. The ColdFusion administrator had a request limit set of 100MB (which I believe is the default). What was up?
It took a bit of googling to realize that IIS7 (the webserver on our Windows 2008 Server), had a default request limit of 30MB. The solution is rather simple. With IIS7, a web.config file can be used to set a variety of webserver settings, so I added one to the site that was having the problem. Here are the contents of the file:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="1024000000"/>
</requestFiltering>
</security>
</system.webServer>
</configuration>
This sets the request size limit to 100MB, to bring it in line with ColdFusion. This isn't the only thing that can be controlled with web.config. For some information on how to tighten security on a site using web.config see this page: http://www.petefreitag.com/item/741.cfm
Comments (0) Posted on May 3, 2010 1:44:47 PM EDT by Catherine Field
Some background first...
I had the need to do diff of html content in a project I was working on, which brought me pretty quickly to DaisyDiff, a really nice Java-based utility. DaisyDiff doesn't however, have a simple built-in function to do a diff of two strings. There is a command-line option, which takes the paths of two files as arguments, and also a java api that take a number of java objects as arguments. What I wanted was a function that took two strings and output the results, but DaisyDiff has no such simple function.
I don't really do java development -- that is I've done some in the past but it's been a while and it would probably take me some time to get my development environment up to snuff. Besides, I didn't really feel like dealing with compiled code.
A quick google search, of course, turns up CFX_CompareHTML and the JavaLoader version of the same thing. So I used that, and it worked fine. But it was using an old version of DaisyDiff, and it seemed to have some bugs with UTF characters and such. What I really wanted to do was to use JavaLoader to load the current version of DaisyDiff. After much stumbling around in the code, I found that the test suite in the DaisyDiff repository has exactly the function I wanted -- it compares two strings and returns the result.
So, long story short, I took the code from that function and pulled it into a CFC, using JavaLoader, and rewrote everything in CFML. The result is the simple function I was after.
So anyway, here it is:
<cfcomponent hint="Wrapper for DaisyDiff" output="false">
<cffunction name="Init" output="false" returntype="DaisyDiff">
<cfargument name="daisydiffpath" hint="absolute path to daisydiff jar file" type="string" required="true">
<cfargument name="javaloaderpath" hint="component path to JavaLoader.cfc" type="string" required="true">
<cfset This.daisydiffpath = arguments.daisydiffpath>
<cfset This.javaloaderpath = arguments.javaloaderpath>
<cfreturn This>
</cffunction>
<cffunction name="Diff" output="false" returntype="string">
<cfargument name="olderHtml" type="string" required="true">
<cfargument name="newerHtml" type="string" required="true">
<cfset var paths = [This.daisydiffpath]>
<cfset var loader = createObject("component", This.javaloaderpath).init(paths)>
<cfset var TransformerFactoryImpl = loader.create("com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl")>
<cfset var StringReader = loader.create("java.io.StringReader")>
<cfset var StringWriter = loader.create("java.io.StringWriter")>
<cfset var Locale = loader.create("java.util.Locale")>
<cfset var StreamResult = loader.create("javax.xml.transform.stream.StreamResult")>
<cfset var OutputKeys = loader.create("javax.xml.transform.OutputKeys")>
<cfset var NekoHtmlParser = loader.create("org.outerj.daisy.diff.helper.NekoHtmlParser")>
<cfset var DomTreeBuilder = loader.create("org.outerj.daisy.diff.html.dom.DomTreeBuilder")>
<cfset var HTMLDiffer = loader.create("org.outerj.daisy.diff.html.HTMLDiffer")>
<cfset var HtmlSaxDiffOutput = loader.create("org.outerj.daisy.diff.html.HtmlSaxDiffOutput")>
<cfset var TextNodeComparator = loader.create("org.outerj.daisy.diff.html.TextNodeComparator")>
<cfset var InputSource = loader.create("org.xml.sax.InputSource")>
<cfset var finalResult = StringWriter.Init()>
<cfset var result = TransformerFactoryImpl.Init().newTransformerHandler()>
<cfset var sr = StreamResult.Init(finalResult)>
<cfset var prefix = "diff">
<cfset var cleaner = NekoHtmlParser.Init()>
<cfset var oldSource = InputSource.Init(StringReader.Init(olderHtml))>
<cfset var newSource = InputSource.Init(StringReader.Init(newerHtml))>
<cfset var oldHandler = DomTreeBuilder.Init()>
<cfset var newHandler = DomTreeBuilder.Init()>
<cfset var leftComparator = "">
<cfset var rightComparator = "">
<cfset var output = "">
<cfset var differ = "">
<cfset var diff = "">
<cfset result.setResult(sr)>
<cfset result.getTransformer().setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes")>
<cfset cleaner.parse(oldSource, oldHandler)>
<cfset leftComparator = TextNodeComparator.Init(oldHandler, Locale.getDefault())>
<cfset cleaner.parse(newSource, newHandler)>
<cfset rightComparator = TextNodeComparator.Init(newHandler, Locale.getDefault())>
<cfset output = HtmlSaxDiffOutput.Init(result,prefix)>
<cfset differ = HTMLDiffer.Init(output)>
<cfset differ.diff(leftComparator, rightComparator)>
<cfset diff = finalResult.toString()>
<cfreturn diff>
</cffunction>
</cfcomponent>
Usage:
<cfset var daisy = CreateObject("component","cfc.DaisyDiff").Init(expandPath("../daisydiff-1.1/daisydiff.jar"),"Lighthouse.Utilities.javaloader.JavaLoader")>
<cfset var diff = daisy.diff(olderhtml,newerhtml)>
The result is html that has been marked up by DaisyDiff with special classes. You can take that and style it in any way that you see fit.
I'm sure there are some refinements that could be done to this CFC. The class name prefix, for instance, is hardcoded to "diff", and that could be changed if you need to use a different prefix. Someone more familiar with the Java classes used here could find problems too, which I would welcome.
Comments (3) Posted on March 29, 2010 4:37:47 PM EDT by Catherine Field