514 lines
12 KiB
Plaintext
514 lines
12 KiB
Plaintext
component
|
|
output = false
|
|
hint = "I provide a way to serialize complex ColdFusion data values as case-sensitive JavaScript Object Notation (JSON) strings."
|
|
{
|
|
//2024-06-27 modified by msyu: added server timezone
|
|
|
|
// I return the initialized component.
|
|
public any function init() {
|
|
|
|
// Every key is added to the full key list - used for one-time key serialization.
|
|
fullKeyList = {};
|
|
|
|
// Every key is added to the hint list so that we don't have to switch on the lists.
|
|
fullHintList = {};
|
|
|
|
// These key lists determine special data serialization.
|
|
stringKeyList = {};
|
|
booleanKeyList = {};
|
|
integerKeyList = {};
|
|
floatKeyList = {};
|
|
dateKeyList = {};
|
|
|
|
// These keys will NOT be used in serialization (ie. the key/value pairs will not be
|
|
// added to the serialized output).
|
|
blockedKeyList = {};
|
|
|
|
// Return the initialized component.
|
|
return( this );
|
|
|
|
}
|
|
|
|
|
|
// ---
|
|
// PUBLIC METHODS.
|
|
// ---
|
|
|
|
|
|
// I define the given key without a type. This is here to provide key-casing without caring
|
|
// about why type of data conversion takes place. Returns serializer.
|
|
public any function asAny( required string key ) {
|
|
|
|
return( defineKey( fullKeyList, key, "any" ) );
|
|
|
|
}
|
|
|
|
|
|
// I define the given key as a boolean. Returns serializer.
|
|
public any function asBoolean( required string key ) {
|
|
|
|
return( defineKey( booleanKeyList, key, "boolean" ) );
|
|
|
|
}
|
|
|
|
|
|
// I define the given key as a date. Returns serializer.
|
|
public any function asDate( required string key ) {
|
|
|
|
return( defineKey( dateKeyList, key, "date" ) );
|
|
|
|
}
|
|
|
|
|
|
// I define the given key as a float / decimal. Returns serializer.
|
|
public any function asFloat( required string key ) {
|
|
|
|
return( defineKey( floatKeyList, key, "float" ) );
|
|
|
|
}
|
|
|
|
|
|
// I define the given key as an integer. Returns serializer.
|
|
public any function asInteger( required string key ) {
|
|
|
|
return( defineKey( integerKeyList, key, "integer" ) );
|
|
|
|
}
|
|
|
|
|
|
// I define the given key as a string. Returns serializer.
|
|
public any function asString( required string key ) {
|
|
|
|
return( defineKey( stringKeyList, key, "string" ) );
|
|
|
|
}
|
|
|
|
|
|
// I define the key as one that should not be included in the serialized response.
|
|
public any function exclude( required string key ) {
|
|
|
|
blockedKeyList[ key ] = true;
|
|
|
|
return( this );
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* I serialize the given input as JavaScript Object Notation (JSON) using the case-sensitive
|
|
* values defined in the key-list.
|
|
*
|
|
* @output false
|
|
*/
|
|
public string function serialize( required any input ) {
|
|
|
|
// Write the serialized value to the output buffer.
|
|
savecontent variable = "local.serializedInput" {
|
|
|
|
serializeInput( input, "any" );
|
|
|
|
}
|
|
|
|
return( serializedInput );
|
|
|
|
}
|
|
|
|
|
|
// ---
|
|
// PRIVATE METHODS.
|
|
// ---
|
|
|
|
|
|
// I define the given key within the given key list.
|
|
private any function defineKey(
|
|
required struct keyList,
|
|
required string key,
|
|
required string hint
|
|
) {
|
|
|
|
if ( structKeyExists( fullKeyList, key ) ) {
|
|
|
|
throw(
|
|
type = "DuplicateKey",
|
|
message = "The key [#key#] has already been defined within the serializer.",
|
|
detail = "The current key list is: #structKeyList( fullKeyList, ', ' )#"
|
|
);
|
|
|
|
}
|
|
|
|
// Add to the appropriate data-type lists. This one is used for existence checking.
|
|
keyList[ key ] = key;
|
|
|
|
// Add all keys to the full key list as well. This one is used to store the serialization
|
|
// of the key so that it doesn't have to be recalculated each time the object is serialized.
|
|
fullKeyList[ key ] = serializeString( key );
|
|
|
|
// If we have a specific type, then add the hint to the full hint list as well. This will
|
|
// allow us to quickly look up the pass-through data type hint during serialization.
|
|
// --
|
|
// NOTE: The reason we don't want to pass through "any" is that we want parent types to be
|
|
// able to "fall through" during the object traversal. If we added "any" to the type list,
|
|
// then it would always overwrite the parent data type.
|
|
if ( hint != "any" ) {
|
|
|
|
fullHintList[ key ] = hint;
|
|
|
|
}
|
|
|
|
// Return this reference for method chaining.
|
|
return( this );
|
|
|
|
}
|
|
|
|
|
|
// I walk the given object, writing the serialized value to the output (which is expected to
|
|
// be a content buffer).
|
|
// ---
|
|
// NOTE: THIS METHOD IS HUGE - this is on purpose. Since serialization is a rather intense
|
|
// process, I am trying to cut out as much overhead as possible. In this case, we're cutting
|
|
// out extra stack space by inlining and duplicating a lot of functionality. This is being done
|
|
// at the COST of clarity and non-repetitive code.
|
|
private void function serializeInput(
|
|
required any input,
|
|
required string hint
|
|
) {
|
|
|
|
// Serialize the data base on the type of input. We are organizing this in terms of the
|
|
// most commonly-used values first. The anticipation is that the vast majority of data
|
|
// types will be simple values.
|
|
if ( isSimpleValue( input ) ) {
|
|
|
|
if ( ( hint == "string" ) || ( hint == "any" ) ) {
|
|
|
|
// If the string appears to be numeric, then we have to prefix it to make sure
|
|
// ColdFusion doesn't accidentally convert it to a number.
|
|
if ( isNumeric( input ) ) {
|
|
|
|
writeOutput( """" & input & """" );
|
|
|
|
} else {
|
|
|
|
serializeInputString( input );
|
|
|
|
}
|
|
|
|
} else if ( ( hint == "boolean" ) && isBoolean( input ) ) {
|
|
|
|
writeOutput( input ? "true" : "false" );
|
|
|
|
} else if ( ( ( hint == "integer" ) || ( hint == "float" ) ) && isNumeric( input ) ) {
|
|
|
|
writeOutput( input );
|
|
|
|
} else if ( ( ( hint == "integer" ) || ( hint == "float" ) ) && isBoolean( input ) ) {
|
|
|
|
writeOutput( input ? "1" : "0" );
|
|
|
|
} else if ( ( hint == "date" ) && ( isDate( input ) || isNumericDate( input ) ) ) {
|
|
|
|
// Write the date in ISO 8601 time string format. We're going to assume that the
|
|
// date is already in the desired timezone.
|
|
///writeOutput( """" & dateFormat( input, "yyyy-mm-dd" ) & "T" & timeFormat( input, "HH:mm:ss.l" ) & "Z""" );
|
|
//2024-06-27 modified by msyu: added server timezone
|
|
writeOutput( """" & dateFormat( input, "yyyy-mm-dd" ) & "T" & timeFormat( input, "HH:mm:ss.lXX" ) & """" );
|
|
|
|
} else {
|
|
|
|
serializeInputString( input );
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
} // END: isSimpleValue().
|
|
|
|
|
|
// I'm expecting the struct to be the next most common data type since it will likely be
|
|
// the container for the majority of data values.
|
|
if ( isStruct( input ) ) {
|
|
|
|
writeOutput( "{" );
|
|
|
|
var isFirst = true;
|
|
|
|
for ( var key in input ) {
|
|
|
|
// Skip any black-listed keys.
|
|
if ( structKeyExists( blockedKeyList, key ) ) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
// Handle the item delimiter.
|
|
if ( isFirst ) {
|
|
|
|
isFirst = false;
|
|
|
|
} else {
|
|
|
|
writeOutput( "," );
|
|
|
|
}
|
|
|
|
// Ensure that the given key can be referenced on the full-key list. This way,
|
|
// the subsequent logic will be easier.
|
|
if ( ! structKeyExists( fullKeyList, key ) ) {
|
|
|
|
asAny( lcase( key ) );
|
|
|
|
}
|
|
|
|
writeOutput( fullKeyList[ key ] & ":" );
|
|
|
|
// Pass in the most appropriate data-type hint based on the parent key.
|
|
if ( structKeyExists( fullHintList, key ) ) {
|
|
|
|
serializeInput( input[ key ], fullHintList[ key ] );
|
|
|
|
// If the given key is unknown, just pass through the most recent hint as
|
|
// it may be defining the type for an entire structure.
|
|
} else {
|
|
|
|
serializeInput( input[ key ], hint );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
writeOutput( "}" );
|
|
|
|
return;
|
|
|
|
} // END: isStruct().
|
|
|
|
|
|
if ( isArray( input ) ) {
|
|
|
|
writeOutput( "[" );
|
|
|
|
var isFirst = true;
|
|
|
|
// Handle the item delimiter.
|
|
for ( var value in input ) {
|
|
|
|
if ( isFirst ) {
|
|
|
|
isFirst = false;
|
|
|
|
} else {
|
|
|
|
writeOutput( "," );
|
|
|
|
}
|
|
|
|
// Since we don't have a key to go off of, pass-through the most recent hint.
|
|
serializeInput( value, hint );
|
|
|
|
}
|
|
|
|
writeOutput( "]" );
|
|
|
|
return;
|
|
|
|
} // END: isArray().
|
|
|
|
|
|
// When we serialize a query, we're going to treat it like an array of structs.
|
|
if ( isQuery( input ) ) {
|
|
|
|
var keys = listToArray( input.columnList );
|
|
|
|
// Make sure each column is defined as a known key - makes the subsequent logic easier.
|
|
for ( var key in keys ) {
|
|
|
|
if ( ! structKeyExists( fullKeyList, key ) ) {
|
|
|
|
asAny( lcase( key ) );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
writeOutput( "[" );
|
|
|
|
// Serialize each row of the query as a struct.
|
|
for ( var i = 1 ; i <= input.recordCount ; i++ ) {
|
|
|
|
// Handle the row delimiter.
|
|
if ( i > 1 ) {
|
|
|
|
writeOutput( "," );
|
|
|
|
}
|
|
|
|
writeOutput( "{" );
|
|
|
|
var isFirst = true;
|
|
|
|
for ( var key in keys ) {
|
|
|
|
// Skip any black-listed keys.
|
|
if ( structKeyExists( blockedKeyList, key ) ) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
// Handle the item delimiter (in the current row).
|
|
if ( isFirst ) {
|
|
|
|
isFirst = false;
|
|
|
|
} else {
|
|
|
|
writeOutput( "," );
|
|
|
|
}
|
|
|
|
writeOutput( fullKeyList[ key ] & ":" );
|
|
|
|
// Pass in the most appropriate data-type hint based on the parent key.
|
|
if ( structKeyExists( fullHintList, key ) ) {
|
|
|
|
serializeInput( input[ key ][ i ], fullHintList[ key ] );
|
|
|
|
// If the given key is unknown, just pass through the most recent hint as
|
|
// it may be defining the type for an entire structure.
|
|
} else {
|
|
|
|
serializeInput( input[ key ][ i ], hint );
|
|
|
|
}
|
|
|
|
} // END: Key list.
|
|
|
|
writeOutput( "}" );
|
|
|
|
} // END: Row list.
|
|
|
|
writeOutput( "]" );
|
|
|
|
return;
|
|
|
|
} // END: isQuery().
|
|
|
|
|
|
// If we made it this far, we were given a data type that we're not actively supporting.
|
|
// As such, we just have to hand this off to the native serializer.
|
|
writeOutput( serializeJson( input ) );
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* I serialize and write the given string to the current output context, escaping all appropriate
|
|
* characters for the JSON specification.
|
|
*
|
|
* NOTE: We are using this manual-encoding process rather than the built-in serializeJson() function
|
|
* as there is a rather nasty bug that corrupts certain patterns in the output. Read more:
|
|
*
|
|
* http://www.bennadel.com/blog/2842-serializejson-and-the-input-and-output-encodings-are-not-same-errors-in-coldfusion.htm
|
|
*
|
|
* @input I am the string being serialized.
|
|
*/
|
|
private void function serializeInputString( required string input ) {
|
|
|
|
// While this may not be technically needed, this will ensure that we are not using any
|
|
// "undocumented features" of the language. If we explicitly cast to a Java string, and
|
|
// something goes wrong due to odd type-casting, it's a ColdFusion bug, at that point, not
|
|
// a logic error ;)
|
|
input = javaCast( "string", input );
|
|
|
|
var length = input.length();
|
|
|
|
writeOutput( """" );
|
|
|
|
for ( var i = 1 ; i <= length ; i++ ) {
|
|
|
|
var charCode = input.codePointAt( javaCast( "int", i - 1 ) );
|
|
|
|
// Check for the most common case first (normal characters).
|
|
if (
|
|
( charCode >= 32 ) &&
|
|
( charCode != 34 ) &&
|
|
( charCode != 47 ) &&
|
|
( charCode != 92 ) &&
|
|
( charCode != 8232 ) &&
|
|
( charCode != 8233 )
|
|
) {
|
|
|
|
writeOutput( chr( charCode ) );
|
|
|
|
// Check for the special cases next (control characters, characters that
|
|
// need to be escaped, and characters that need to be encoded nicely).
|
|
} else if ( charCode == 8 ) {
|
|
|
|
writeOutput( "\b" );
|
|
|
|
} else if ( charCode == 9 ) {
|
|
|
|
writeOutput( "\t" );
|
|
|
|
} else if ( charCode == 10 ) {
|
|
|
|
writeOutput( "\n" );
|
|
|
|
} else if ( charCode == 12 ) {
|
|
|
|
writeOutput( "\f" );
|
|
|
|
} else if ( charCode == 13 ) {
|
|
|
|
writeOutput( "\r" );
|
|
|
|
} else if (
|
|
( charCode < 32 ) ||
|
|
( charCode == 8232 ) ||
|
|
( charCode == 8233 )
|
|
) {
|
|
|
|
// For Unicode hex values, we need to enforce a 4-digit code.
|
|
writeOutput( "\u" & right( ( "000" & formatBaseN( charCode, 16 ) ), 4 ) );
|
|
|
|
} else if ( charCode == 34 ) {
|
|
|
|
writeOutput( "\""" );
|
|
|
|
} else if ( charCode == 47 ) {
|
|
|
|
writeOutput( "\/" );
|
|
|
|
} else if ( charCode == 92 ) {
|
|
|
|
writeOutput( "\\" );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
writeOutput( """" );
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* I serialize and return the given string, escaping all appropriate characters for the
|
|
* JSON specification.
|
|
*
|
|
* @input I am the string being serialized.
|
|
* @output false
|
|
*/
|
|
private string function serializeString( required string input ) {
|
|
|
|
savecontent variable = "local.json" {
|
|
|
|
serializeInputString( input );
|
|
|
|
}
|
|
|
|
return( json );
|
|
|
|
}
|
|
|
|
} |