Problems with creating an array of structs - coldfusion

I'm making a blog API and am having some very strange issues when trying to create an array of structs in coldfusion. The top level array will contain the post, as a struct, with a .comments that is an array of all comments under that post, also as structs.
Each of the pieces in the following code work individually. But, somehow when I put them together I end up with an infinitely nested array of structs containing an array of structs etc... all of only the very last element in the top level array of posts.
<cfset posts = VARIABLES.postDao.getBlogPosts(argumentCollection=arguments) />
<cfset result = arraynew(1) />
<cfloop index="i" from="1" to="#arrayLen(posts)#">
<cfset post = posts[i].getInstance()>
<cfset StructInsert(post, 'comments', getComments(post.postId))>
<cfset ArrayAppend(result, post)>
</cfloop>
getBlogPosts returns an array of Post beans.
bean.getInstance() returns a struct with all the data in the bean.
getComments(id) returns an array all comments(structs) for post[id].
Each of these works as intended and is used elsewhere without problems.
The structure of the infinitely nested array is as such:
Array containing Post
. Post.comments containing array of comments + Post on end
. . Post.comments containing array of comments + Post on end
. . . etc...

You didn't show the entire code.
I suspect replacing what you did show with either of these will solve the problem:
<cfset local.posts = VARIABLES.postDao.getBlogPosts(argumentCollection=arguments) />
<cfset local.result = arraynew(1) />
<cfloop index="local.i" from="1" to="#arrayLen(local.posts)#">
<cfset local.post = local.posts[local.i].getInstance()>
<cfset StructInsert(local.post, 'comments', getComments(local.post.postId))>
<cfset ArrayAppend(local.result, local.post)>
</cfloop>
Or:
<cfset var posts = VARIABLES.postDao.getBlogPosts(argumentCollection=arguments) />
<cfset var result = arraynew(1) />
<cfset var i = 0 />
<cfset var post = 0 />
<cfloop index="i" from="1" to="#arrayLen(posts)#">
<cfset post = posts[i].getInstance()>
<cfset StructInsert(post, 'comments', getComments(post.postId))>
<cfset ArrayAppend(result, post)>
</cfloop>
You should always use either var keyword or local scope for variables in a cffunction.
You can use VarScoper to check your code for other places where this needs fixing.

Please try adding some cfdumps in there and report back what you get:
<cfset posts = VARIABLES.postDao.getBlogPosts(argumentCollection=arguments) />
<cfset result = arraynew(1) />
<cfloop index="i" from="1" to="#arrayLen(posts)#">
<cfset post = posts[i].getInstance()>
<cfdump var="#post#">
<cfset StructInsert(post, 'comments', getComments(post.postId))>
<cfdump var="#post#">
<cfset ArrayAppend(result, post)>
<cfdump var="#result#"><cfabort>
</cfloop>
edit
I think the problem is a child reference to a parent value, which spawns the infinite loop when recursing through the object. Try changing to this:
<cfset posts = VARIABLES.postDao.getBlogPosts(argumentCollection=arguments) />
<cfset result = arraynew(1) />
<cfloop index="i" from="1" to="#arrayLen(posts)#">
<cfset post = posts[i].getInstance()>
<cfset StructInsert(post, 'comments', Duplicate(getComments(post.postId)))>
<cfset ArrayAppend(result, post)>
</cfloop>

Related

CFML - Parse a String - Find Variables

I know this is fairly simple. But I can't find my cut paste code or something that works.
I am encrypting a string.
<cfset encrlink = encrypt("uid=WWWDDD&email=BOBEMAIL#BOB.COM", APPLICATION.AesKeyForMails, "AES", "HEX")>
Which works fine. And then I decrypt it.
<cfset dncrlink = decrypt("#encrlink#", APPLICATION.AesKeyForMails, "AES", "HEX")>
<cfoutput>[#dncrlink#]</cfoutput>
Which works fine. Gives out "uid=WWWDDD&email=BOBEMAIL#BOB.COM"
I need to get "uid=" and "email=" out and use as variables of the decrypted string. By parsing. I'm just a bit stuck. Is it Trim? Find? I kinda have this working. But not sure how to build the cfset.
<cfoutput>
<cfset theUrl = "#dncrlink#" />
<cfloop list="#theUrl#" index="URLPiece" delimiters="&">
<br>#listFirst(urlPiece, "=")# = #listLast(urlPiece, "=")# <br>
</cfloop>
</cfoutput>
I was able to do a CFIF for the Variables.
<cfloop list="#theUrl#" index="URLPiece" delimiters="&">
<br>#listFirst(urlPiece, "=")# = #listLast(urlPiece, "=")# <br>
<cfif #listFirst(urlPiece, "=")# is "uid"><cfset uid = "#listLast(urlPiece, "=")#"></cfif>
<cfif #listFirst(urlPiece, "=")# is "email"><cfset email = "#listLast(urlPiece, "=")#"></cfif>
</cfloop>
Too long for comments ...
You could also "genericize" the logic into something which could be used with any query string. Extract the parameters and store them in a structure
<cfset dncrlink = "uid=WWWDDD&email=BOBEMAIL#BOB.COM">
<cfset params = {}>
<cfloop list="#dncrlink#" index="elem" delimiters="&">
<cfset params[ elem.listFirst("=") ] = elem.listRest("=")>
</cfloop>
Then to access parameter values:
<cfoutput>
uid = #params.uid#<br>
email = #params.email#<br>
</cfoutput>

in Coldfusion how do I add a 'column' to the struct inside an array?

For example I have array ads and inside it has images
ads
images
url = someurl
title = sometitle
*here I want to add prop = someprop
images
url = someurl
title = sometitle
*here I want to add prop = someprop
ads
images
url = someurl
title = sometitle
*here I want to add prop = someprop
Now I loop
<cfloop array="ads" index="i">
<cfloop array="i.images" index="j">
<cfif somecondition>
<cfset prop = 'yes'>
<cfelse>
<cfset prop = 'no'>
</cfif>
<cfset ArrayAppend(i.images[j],prop)> ???
</cfloop>
</cfloop>
I tried several things with the ArrayAppend, tried a StructAppend, but apparently I am unable to understand how I would go about.
You were close. You can simply set the prop on the item in the images loop. This is assuming that the items in the i.images array are structures.
<cfloop array="ads" index="i">
<cfloop array="i.images" index="j">
<cfif somecondition>
<cfset j.prop = 'yes'>
<cfelse>
<cfset j.prop = 'no'>
</cfif>
</cfloop>
</cfloop>
I'm not aware of an array attribute for cfloop, but I think this is what you're after
<cfloop from="1" to="#arrayLen(ads)#" index="i" step="1">
<cfset ads[i]["newprop"] = newpropvalue>
</cfloop>

Coldfusion 10 - Element [n] is undefined in a Java object of type class coldfusion.runtime.Array

I recently upgraded a system from CF8 to CF10 and have one bug that I'm having problems tracking down. It has to do with a remote API call that gets a JSON string back and that string then gets converted to a query object. That's where I'm coming across the error:
Element [n] is undefined in a Java object of type class coldfusion.runtime.Array. The problem is in the function that converts the string to a query.
<cffunction name="CFjsonToQuery" access="public" returntype="query" output="no">
<cfargument name="cfData" required="yes" type="struct"/>
<cfset var LOCAL = {}/>
<cfset LOCAL.tmpQry = QueryNew( ArrayToList(ARGUMENTS.cfData.Data.COLUMNS) ) />
<cfloop index = "i" from = "1" to = "#ArrayLen(ARGUMENTS.cfData.Data.DATA)#">
<cfset LOCAL.Row = QueryAddRow(LOCAL.tmpQry) />
<cfloop index="k" from="1" to="#ArrayLen(ARGUMENTS.cfData.Data.DATA[i])#">
<cfset LOCAL.colName = ARGUMENTS.cfData.Data.COLUMNS[K]/>
<cfset QuerySetCell(LOCAL.tmpQry,LOCAL.colName,ARGUMENTS.cfData.Data.DATA[i][k],LOCAL.Row)/>
</cfloop>
</cfloop>
<cfreturn LOCAL.tmpQry/>
</cffunction>
Anywhere the JSON returns 'null' (i.e. "...","19107-3609",null,null,null,"...") the error is thrown. I've tried using isNull to check if it's null in the cfloop:
<cfif isNull(ARGUMENTS.cfData.Data.DATA[i][k])>
<cfset ARGUMENTS.cfData.Data.DATA[i][k] = 'I AM NULL'/>
</cfif>
EDIT - here's a simplified example - the issue is the way the newer deserializeJson() works I believe:
<cfset jstr = '{"SUCCESS":true,"ERRORS":[],"DATA":{"COLUMNS":["ID","FNAME","LNAME"],"DATA":[[390132,"steve",null]]}}'/>
<cfset cfData = deserializeJson(jstr) />
<cfloop index = "i" from = "1" to = "#ArrayLen(cfData.Data.DATA)#">
<cfset Row = QueryAddRow(tmpQry) />
<cfloop index="k" from="1" to="#ArrayLen(cfData.Data.DATA[i])#">
<cfset colName = cfData.Data.COLUMNS[K]/>
<cfset QuerySetCell(tmpQry,colName,cfData.Data.DATA[i][k],Row)/>
</cfloop>
</cfloop>
I've tried all sorts of tests for empty string, isNull etc. and I'm still not sure how to get the query object built if deserializejson returns:
[undefined array element] Element 3 is undefined in a Java object of type class coldfusion.runtime.Array.
This does seem to work:
<cfset cfData = deserializeJson(returnData,'FALSE') />
<cfset qryData = cfData.data />
This lets me then use qryData as if it were a normal cfquery.
You can do a check if the element is undefined using the CF Function ArrayIsDefined(array, elementIndex)
What I've done for now is add 'FALSE' to the deserializeJSON strictMapping flag and that seems to automatically create a query object? I'll admit though this is getting into the underpinnings of CF10 and I could be wrong on that. I'll update my code above for visual clarity.

CFScript Evaluate() throws error in loop

I'm attempting to dump a bunch of get functions into a spreadsheet using cfspreadsheet - instead of passing each individual function, I've decided to create a list and loop through it. I think I am using Evaluate() incorrectly here, but I'm not sure what the best way to accomplish this is. Any suggestions/optimizations would be appreciated, as my Cold-Fu isn't that great.
Error thrown is Variable GETFIELDS is undefined.
<cfset var fields = "Function1,Function2" />
<cfspreadsheet action="read" src="#strDestinationPath#information.xls" name="xlsInfo" headerrow="1" />
<cfset var row = xlsData.rowcount + 1 />
<cfset var count = 1 />
<cfloop list="fields" index="f" delimiters=",">
<cfscript>
SpreadsheetSetCellValue(xlsInfo,Evaluate('get' & f & '()'),row,count);
count++;
</cfscript>
</cfloop>
<cfspreadsheet action="write" overwrite="true" filename="#strDestinationPath#information.xls" name="xlsInformation" />
cfloop expects a list of items as an argument.
Try changing from
<cfloop list="fields" index="f" delimiters=",">
to
<cfloop list="#fields#" index="f" delimiters=",">
or to
<cfloop list="Function1,Function2" index="f" delimiters=",">

Consuming a webservice code simplification

UPDATED CODE TO LATEST ITERATION
The following function consumes a webservice that returns address details based on zip code (CEP). I'm using this function to parse the xml and populate an empty query with the address details. I would like to know if there is a more elegant way to achieve the same result. It seems to be a waste to create an empty query and populate it...
Any ideas could my method be modified or the code factored/simplified?
<!--- ****** ACTION: getAddress (consumes web-service to retrieve address details) --->
<cffunction name="getAddress" access="remote" returntype="any" output="false">
<!--- Defaults: strcep (cep (Brazilian zip-code) string webservice would look for), search result returned from webservice --->
<cfargument name="cep" type="string" default="00000000">
<cfset var searchResult = "">
<cfset var nodes = "">
<cfset var cfhttp = "">
<cfset var stateid = 0>
<cfset var tmp = structNew()>
<!--- Validate cep string --->
<cfif IsNumeric(arguments.cep) AND Len(arguments.cep) EQ 8>
<cftry>
<!--- Consume webservice --->
<cfhttp method="get" url="http://www.bronzebusiness.com.br/webservices/wscep.asmx/cep?strcep=#arguments.cep#"></cfhttp>
<cfset searchResult = xmlparse(cfhttp.FileContent)>
<cfset nodes = xmlSearch(searchResult, "//tbCEP")>
<!--- If result insert address data into session struct --->
<cfif arrayLen(nodes)>
<cfset tmp.streetType = nodes[1].logradouro.XmlText>
<cfset tmp.streetName = nodes[1].nome.XmlText>
<cfset tmp.area = nodes[1].bairro.XmlText>
<cfset tmp.city = nodes[1].cidade.XmlText>
<cfset tmp.state = nodes[1].uf.XmlText>
<cfset tmp.cep = arguments.cep>
<!--- Get state id and add to struct --->
<cfset stateid = model("state").findOneByStateInitials(tmp.state)>
<cfset tmp.stateid = stateid.id>
<cfreturn tmp>
</cfif>
<!--- Display error if any --->
<cfcatch type="any">
<cfoutput>
<h3>Sorry, but there was an error.</h3>
<p>#cfcatch.message#</p>
</cfoutput>
</cfcatch>
</cftry>
</cfif>
</cffunction>
<!--- ****** END ACTION getAddress --->
The calling code:
<!--- Get address data based on CEP --->
<cfset session.addressData = getAddress(cep=params.newMember.cep)>
I can't test this because I don't have an example XML file / CEP to test with, but here is a minor rewrite that addresses four things:
Instead of using cfparam and some strange "params" structure, you should pass the CEP into the function as an argument.
The function shouldn't directly modify session data. Instead, you should return the result and let the calling code assign it to the session (or wherever else it might be needed). I'll show this in a 2nd code example.
Cache the xml result per CEP -- assuming this doesn't change often. (You'll have to improve it further if you want time-based manual cache invalidation, but I can help add that if necessary)
Don't use StructInsert. It's not necessary and you're just writing it the long way for the sake of writing it the long way. There is no benefit.
Again, this isn't tested, but hopefully it's helpful:
<cffunction name="getAddress" access="remote" returntype="any" output="false">
<cfargument name="cep" type="string" default="00000000" /><!--- (cep (Brazilian zip-code) string webservice would look for) --->
<cfset var searchResult = "">
<cfset var nodes = "">
<cfset var cfhttp = "">
<cfset var stateid = 0 />
<cfset var tmp = structNew()>
<!--- Validate cep string --->
<cfif IsNumeric(arguments.cep) AND Len(arguments.cep) EQ 8>
<cfif not structKeyExists(application.cepCache, arguments.cep)><!--- or cache is expired: you'd have to figure this part out --->
<!--- Consume webservice --->
<cftry>
<cfhttp method="get" url="http://www.bronzebusiness.com.br/webservices/wscep.asmx/cep?strcep=#arguments.cep#" />
<cfset searchResult = xmlparse(cfhttp.FileContent)>
<cfset nodes = xmlSearch(searchResult, "//tbCEP")>
<!--- If result insert address data into session struct --->
<cfif arrayLen(nodes)>
<cfset tmp.streetType = nodes[1].logradouro.XmlText />
<cfset tmp.streetName = nodes[1].nome.XmlText />
<cfset tmp.area = nodes[1].bairro.XmlText />
<cfset tmp.city = nodes[1].cidade.XmlText />
<cfset tmp.state = nodes[1].uf.XmlText />
<cfset tmp.cep = arguments.cep />
<!--- Get state id and add to struct --->
<cfset stateid = model("state").findOneByStateInitials(session.addressData.state)>
<cfset tmp.stateid = stateid.id />
</cfif>
<cfreturn duplicate(tmp) />
<!--- Display error if any --->
<cfcatch type="any">
<h3>Sorry, but there was an error.</h3>
<p>#cfcatch.message#</p>
</cfcatch>
</cftry>
<cfelse>
<!--- cache exists and is not expired, so use it --->
<cfreturn duplicate(application.cepCache[arguments.cep]) />
</cfif>
</cfif>
<!---
<!--- Redirect to page two of the sign up process --->
<cfset redirectTo(controller="assine", action="perfil")>
--->
</cffunction>
Notice that I commented out the redirect you had at the end. That's because with my function, you'll be returning a value, and the redirect should be done after that, like so:
<cfset session.addressData = getAddress("some-CEP-value") />
<cfset redirectTo(controller="assine", action="perfil")>
If you're going to leave out the caching (As you say in a comment you will), then here is a version that makes no attempt at caching:
<cffunction name="getAddress" access="remote" returntype="any" output="false">
<cfargument name="cep" type="string" default="00000000" /><!--- (cep (Brazilian zip-code) string webservice would look for) --->
<cfset var searchResult = "">
<cfset var nodes = "">
<cfset var cfhttp = "">
<cfset var stateid = 0 />
<cfset var tmp = structNew()>
<!--- Validate cep string --->
<cfif IsNumeric(arguments.cep) AND Len(arguments.cep) EQ 8>
<!--- Consume webservice --->
<cftry>
<cfhttp method="get" url="http://www.bronzebusiness.com.br/webservices/wscep.asmx/cep?strcep=#arguments.cep#" />
<cfset searchResult = xmlparse(cfhttp.FileContent)>
<cfset nodes = xmlSearch(searchResult, "//tbCEP")>
<!--- If result insert address data into session struct --->
<cfif arrayLen(nodes)>
<cfset tmp.streetType = nodes[1].logradouro.XmlText />
<cfset tmp.streetName = nodes[1].nome.XmlText />
<cfset tmp.area = nodes[1].bairro.XmlText />
<cfset tmp.city = nodes[1].cidade.XmlText />
<cfset tmp.state = nodes[1].uf.XmlText />
<cfset tmp.cep = arguments.cep />
<!--- Get state id and add to struct --->
<cfset stateid = model("state").findOneByStateInitials(session.addressData.state)>
<cfset tmp.stateid = stateid.id />
</cfif>
<cfreturn duplicate(tmp) />
<!--- Display error if any --->
<cfcatch type="any">
<h3>Sorry, but there was an error.</h3>
<p>#cfcatch.message#</p>
</cfcatch>
</cftry>
</cfif>
<!---
<!--- Redirect to page two of the sign up process --->
<cfset redirectTo(controller="assine", action="perfil")>
--->
</cffunction>
Note that I did leave in the use of duplicate(). What this does is return a duplicate of the object (in this case, the struct). This is much more important when you start to work on applications where you're passing complex values into and out of functions over and over again. Using duplicate() causes things to be passed by value instead of by reference. It may not bite you in this case, but it's a good habit to get into.
I would also still use the function argument and return a value -- but it's arguable that this is my personal preference. In a way it is. I believe that a function should be fully encapsulated; a total "black box". You give it some input and it gives you back some output. It should not modify anything outside of itself. (Again, just my opinion.)
So assuming you're using this function as part of a larger multi-step process, you should still use it the same way I've described above. The only difference is that you're setting the session variable outside of the function body. Just as previously:
<cfset session.addressData = getAddress("some-CEP-value") />
<cfset redirectTo(controller="assine", action="perfil")>
That looks pretty straightforward. CF doesn't (yet?) have any magical XML-to-Query functions, but that would be pretty cool. If you wanted, you could probably write up an XSL transform to go from XML to WDDX so that you could use the cfwddx tag ... but that's probably putting the cart before the horse.
You need to move your arrayLen() if block into the try block. As it stands, if the cfhttp tag throws an error, the nodes variable will be a string and not an array, thus causing the arrayLen() to throw another error.
Minor nitpick: I wouldn't add a row to the query until inside the arrayLen() block. That way, the calling code can check recordCount to see if the result was a success.
Beyond that ... that's pretty much how it's done.