Skip to main content
Ben Nadel at the New York ColdFusion User Group (Jun. 2010) with: Andy Matthews and Clark Valberg and Ralf Korinth
Ben Nadel at the New York ColdFusion User Group (Jun. 2010) with: Andy Matthews Clark Valberg Ralf Korinth

Polyfill URL Search Parameter Grouping Using Bracket Notation In Adobe ColdFusion

By
Published in Comments (2)

A few months ago, I wrote about how to polyfill Lucee CFML's form field grouping behavior using field names that end with []. It's such a great feature and I use it all the time for forms that have sets of related checkboxes and text fields. But, after chatting with Mary Jo Sminkey earlier this week, I wondered if the same technique could be applied to the URL search parameters.

The great news is, Lucee CFML supports this natively. Presumably, whatever they're doing for form fields behind the scenes, they're also doing for query string parameters.

And, the good news is, I was able to get this polyfilled in Adobe ColdFusion as well. It took some trial and error because the form and url parameters (from the underlying servlet) show up in different places depending on the HTTP method (GET vs POST). Not quite sure why that's the case — I'm not a Java guy. I'm assuming that's just how the servlet works?

Here's the test ColdFusion page that I used to get this working. It has three forms on it:

  • All parameters submitted via GET.
  • All parameters submitted via POST.
  • Parameters submitted via GET and POST.

In all cases, some subset of parameters are submitted using bracket notation for grouping. And I'm outputting the url and form scopes at the bottom of the processing:

<h2>
	GET Parameters Only
</h2>

<form method="get" action="test.cfm">
	<input type="hidden" name="step" value="colorSelection" />
	<input type="hidden" name="colors[]" value="deeppink" />
	<input type="hidden" name="colors[]" value="fuschia" />
	<input type="hidden" name="colors[]" value="rosegold" />

	<button type="submit">
		Submit (URL)
	</button>
</form>

<h2>
	POST Parameters Only
</h2>

<form method="post" action="test.cfm">
	<input type="hidden" name="step" value="colorSelection" />
	<input type="hidden" name="colors[]" value="deeppink" />
	<input type="hidden" name="colors[]" value="fuschia" />
	<input type="hidden" name="colors[]" value="rosegold" />

	<button type="submit">
		Submit (FORM)
	</button>
</form>

<h2>
	Both GET And POST Parameters
</h2>

<form method="post" action="test.cfm?currentStep=colorSelection&completedSteps[]=one&completedSteps[]=two">
	<input type="hidden" name="colors[]" value="deeppink" />
	<input type="hidden" name="colors[]" value="fuschia" />
	<input type="hidden" name="colors[]" value="rosegold" />

	<button type="submit">
		Submit (URL + FORM)
	</button>
</form>

<hr />

<cfdump
	var="#url#"
	label="URL scope"
/>
<cfdump
	var="#form#"
	label="FORM scope"
/>

Here's a stitched-together screenshot of all three forms being submitted:

Screenshot of browser after all three form submissions.

As you can see, the [] group notation worked perfectly in all three scenarios, including the last one in which both url and form scope parameters are being grouped in the same request.

While this works natively in Lucee CFML, I'm using the Application.cfc ColdFusion component to polyfill this behavior in the onRequestStart() life-cycle event:

component
	output = false
	hint = "I define the application settings and event handlers."
	{

	// Define the application settings.
	this.name = "PolyfillInputGrouping";
	this.applicationTimeout = createTimeSpan( 1, 0, 0, 0 );
	this.sessionManagement = false;
	this.setClientCookies = false;
	this.passArrayByReference = true;

	// ---
	// LIFE-CYCLE METHODS.
	// ---

	/**
	* I initialize each inbound HTTP request.
	*/
	public void function onRequestStart() {

		// Note: in a production environment, this CFC can be cached.
		new ParameterGroupingPolyfill().apply();

	}

}

The Application.cfc is just using the life-cycle events to make sure the polyfill gets applied. But, the logic for the polyfill is all contained within the ParameterGroupingPolyfill.cfc component. Since this is just a demo, I'm instantiating the CFC on every request. But, in a production environment, I would cache it in a persisted scope.

Here's the logic for the polyfill that works for both form and url grouping. It short-circuits the logic for Lucee CFML since it's not needed; and, it throws an error in Boxlang since I couldn't figure out how to access the underlying raw parameters:

component
	hint = "I try to polyfill the parameter grouping functionality used by Lucee CFML"
	{

	/**
	* I initialize the polyfill component.
	*/
	public void function init() {

		variables.engines = {
			ADOBE: "Adobe",
			LUCEE: "Lucee",
			BOXLANG: "Boxlang"
		};

		this.ENGINE = determineCfmlEngine();
		this.IS_ADOBE = ( this.ENGINE == engines.ADOBE );
		this.IS_LUCEE = ( this.ENGINE == engines.LUCEE );
		this.IS_BOXLANG = ( this.ENGINE == engines.BOXLANG );

	}

	// ---
	// PUBLIC METHODS.
	// ---

	/**
	* I apply the parameter grouping polyfill to the CFML scopes.
	*/
	public void function apply() {

		// Lucee already supports parameter grouping in both the form fields and the
		// search parameters. As such, nothing needs to be done on a Lucee server.
		if ( this.IS_LUCEE ) {

			return;

		}

		// Boxlang doesn't appear to expose the underlying parameter map in the servlet
		// wrapper. As such, we aren't able to polyfill the feature at this time (at least
		// not until I talk to Brad Wood).
		if ( this.IS_BOXLANG ) {

			throw(
				type = "Unsupported",
				message = "Boxlang can't be polyfilled at this time.",
				detail = "I haven't figured out how to access the raw request yet."
			);

		}

		// Inspect the URL scope.
		var keysToFix = findKeysToFix( url );

		if ( keysToFix.len() ) {

			fixScopeKeys( url, keysToFix, getRawUrlScope() );

		}

		// Inspect the FORM scope.
		var keysToFix = findKeysToFix( form );

		if ( keysToFix.len() ) {

			fixScopeKeys( form, keysToFix, getRawFormScope() );
			fixFieldNames();

		}

	}

	// ---
	// PRIVATE METHODS.
	// ---

	/**
	* I determine which CFML engine is running.
	*/
	private string function determineCfmlEngine() {

		if ( server.keyExists( "lucee" ) ) {

			return engines.LUCEE;

		}

		if ( server.keyExists( "boxlang" ) ) {

			return engines.BOXLANG;

		}

		return engines.ADOBE;

	}


	/**
	* I identify which keys in the given scope need to be fixed. These are the keys that
	* still have the "[]" suffix, indicating that the CFML engine didn't handle the
	* grouping properly.
	*/
	private array function findKeysToFix( required struct cfmlScope ) {

		return cfmlScope
			.keyArray()
			.filter( ( key ) => ( key.right( 2 ) == "[]" ) )
		;

	}


	/**
	* I remove any "[]" notation from the fieldnames property of the form.
	*/
	private void function fixFieldNames() {

		form.fieldNames = form.fieldNames
			.reReplace( "\[\](,|$)", "\1", "all" )
		;

	}


	/**
	* I replace the list-based value concatenation of the CFML scope with the array-based
	* value aggregation in the given raw servlet parameters.
	*/
	private void function fixScopeKeys(
		required struct cfmlScope,
		required array keysToFix,
		required struct rawParameters
		) {

		for ( var key in keysToFix ) {

			// Remove the "[]" suffix from the key and create a new entry.
			cfmlScope[ key.left( -2 ) ] =
				// The underlying Java value is a native Java array. We need to convert
				// that value to a native ColdFusion array (ArrayList) so that it will
				// behave like any other array, complete with member methods.
				arrayNew( 1 ).append( rawParameters[ key ], true )
			;

			// Swap the raw scope key-value pairs with the normalized versions.
			cfmlScope.delete( key );

		}

	}


	/**
	* I get the underlying servlet form parameters.
	* 
	* Caution: at this time, we assume this is the Adobe ColdFusion engine servlet
	* implementation since it's the only one we can polyfill at this time.
	*/
	private struct function getRawFormScope() {

		// Note: we're creating an intermediary struct in order to convert the raw servlet
		// parameters into a CASE-INSENSITIVE collection. Without this, the key-casing in
		// the corresponding CFML scope may not be accessible in the raw parameters.
		var caseInsensitive = {};

		if ( isGet() ) {

			return caseInsensitive;

		}

		return caseInsensitive
			.append( getServletRequest().getParameterMap() )
		;

	}


	/**
	* I get the underlying servlet search parameters.
	* 
	* Caution: at this time, we assume this is the Adobe ColdFusion engine servlet
	* implementation since it's the only one we can polyfill at this time.
	*/
	private struct function getRawUrlScope() {

		// Note: we're creating an intermediary struct in order to convert the raw servlet
		// parameters into a CASE-INSENSITIVE collection. Without this, the key-casing in
		// the corresponding CFML scope may not be accessible in the raw parameters.
		var caseInsensitive = {};

		if ( isPost() ) {

			return caseInsensitive
				.append( getServletRequest().getRequest().getParameterMap() )
			;

		}

		// Note: For a GET request, this will contain just the URL parameters. However,
		// for PUT/PATCH/DELETE, this will be a combination of both URL and FORM values.
		return caseInsensitive
			.append( getServletRequest().getParameterMap() )
		;

	}


	/**
	* I get the underlying servlet request.
	*/
	private any function getServletRequest() {

		return getPageContext().getRequest();

	}


	/**
	* I determine if the current HTTP request is a get.
	*/
	private boolean function isGet() {

		return ( cgi.request_method == "get" );

	}


	/**
	* I determine if the current HTTP request is a post.
	*/
	private boolean function isPost() {

		return ( cgi.request_method == "post" );

	}

}

I'll talk to the Ortus guys and see if I can get some insight in how to polyfill this for Boxlang.

Want to use code from this post? Check out the license.

Reader Comments

16,109 Comments

@Mary Jo,

Agreed - I'm not sure I've ever seen bracket-notation in a URL outside of a PHP app. But, good to know that it's possible. And, odd that it was relatively difficult to figure out - not sure why Adobe moves things around based on GET vs POST.

Post A Comment — I'd Love To Hear From You!

Post a Comment

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel
Managed hosting services provided by:
xByte Cloud Logo