"CL" recently asked if I could show an example of searching a directory using JLDAP to return member attributes. It's pretty straightforward, although getting the results out of the com.novell.ldap.LDAPSearchResult is not particularly easy. Say we have a fully instantiated and connected LDAP connection object (com.novell.ldap.LDAPConnection) named lc. Also assume we have bound to the directory (lc.bind(userDn,password)) with credentials that have the rights to perform a search.
First, we need an array of java.lang.String to let JLDAP know what attributes we are interested in. The easiest way to do that is take a comma separated list and use java's split() function:
<cfset attributeArray = createobject("java", "java.lang.String").init(attributelist).split(",")/>
We also need a string to represent the start/base container of the search, a search filter, and we need to identify what scope the search should be (base, onelevel, or subtree). Most of this is identical to what goes into the CFLDAP tag. A JLDAP search call would look like this:
<cfset resultObj = lc.search("ou=MyOU,o=MyO",
lc.SCOPE_ONE,
"cn=SomeCn",
attributeArray,
false)/>
The last argument specifies whether you want JLDAP to actually get the values of the attributes you specified, for each LDAP entry it matches. Passing false says that it should (this option probably exists because you could separately do a read operation on each matching entry).
Now, the tougher part is actually using your search results, which is basically a collection of com.novell.ldap.LDAPEntry. To iterate over the collection, we would use the resultObj.next() method, which returns the next LDAPEntry object in the collection. To use a cfloop, we can use the .hasMore() method, indicating whether we have iterated over the entire collection:
<cfloop condition="#resultObj.hasMore()#">
<cfset ldapEntry = resultObj.next()/>
<!--- process entry code here... --->
</cfloop>
Inspecting a LDAPEntry is slightly difficult... it doesn't let you know which attributes exist, and it will return null if you try to access one that isn't there. CFMX barfs on null values (something that needs a fixin'), so you need to use try/catch. The LDAPEntry also can do some interesting things based on the type of attribute. For instance, most of the directory work I do revolves around a user's group membership. CFLDAP will typically give you a comma-separated list if an LDAP attribute has more than one value. This makes multi-valued attributes really hard to parse, because each value has many commas in it. The LDAPEntry class actually knows how many values an attribute has (by calling .size()), and it can give them to you in an array if you like, which makes them much easier to work with. This function I wrote will take a LDAPSearchResult and an String[] attribute array, and will convert them into a cfquery. Any attributes it can't find are left as nulls in the query. The cool thing is that the multi-valued attributes actually go into the query as arrays:
<cffunction name="resultsToQuery" returntype="query">
<cfargument name="attributeArray" type="array" required="true"/>
<cfargument name="searchResults" type="any" required="true"/>
<cfset var rtnQry = queryNew(arraytolist(arguments.attributeArray))/>
<cfset var rtnEntry = 0/>
<cfset var rc = 0/>
<cfset var a = 0/>
<cfloop condition="#arguments.searchResults.hasMore()#">
<cfset rtnEntry = arguments.searchResults.next()/>
<cfset rc = rc + 1 />
<cfset queryAddRow(rtnQry,1)/>
<cfloop from="1" to="#arraylen(arguments.attributeArray)#" index="a">
<cftry>
<cfif arguments.attributeArray[a] eq "dn">
<cfset querySetCell(rtnQry,arguments.attributeArray[a],rtnEntry.getDN())/>
<cfelseif rtnEntry.getAttribute(arguments.attributeArray[a]).size() gt 1 >
<cfset querySetCell(rtnQry,arguments.attributeArray[a],rtnEntry.getAttribute(arguments.attributeArray[a]).getStringValueArray(),rc)/>
<cfelse>
<cfset querySetCell(rtnQry,arguments.attributeArray[a],rtnEntry.getAttribute(arguments.attributeArray[a]).getStringValue(),rc)/>
</cfif>
<cfcatch>
<!--- do nothing --->
</cfcatch>
</cftry>
</cfloop>
</cfloop>
<cfreturn rtnQry/>
</cffunction>
The only thing to note is that you can't access an LDAPEntry's dn by .getAttribute('dn'), you have to call .getDN().
Now, you may be wondering, "I thought this post was about ASYNCRHRONOUS searching?". Well, in answering CL's question, I got interested in whether you could use JLDAP's async' search methods from CFMX, and whether there are any advantages. I have yet to REALLY test this (e.g. w/ 1000's of records), but it does seem to hit the result processing code immediately after the search call, which is the desired effect. One thing I might play with is placing the result "queue" into a shared scope, and allow the user to page through it at their own pace (unbeknownst to them that the server is still adding results to it).
The main roadblock I hit was (gasp) CF's handing of nulls. If you look at the documentation, and sample code, LDAPConnection basically wants a null value (but cast to LDAPSearchQueue) to be passed in. This really isn't possible directly with CFMX, and trust me, I tried a bunch of things. So I decided to write a quick java helper class to enable this functionality:
package
com.dross.novell;There's still more to do here, but I think you get the idea. I also think that I may be sharing search queues across requests (by using the default search queue), so I will have to tweak that as well. Anyways, I jar'd that class up (called it ldapSearchAS), and placed it in WEB-INF/lib. Now I can do async searches from CFMX like:
<cfset asSearchObj =createobject("java", "com.dross.novell.ldapSearchAS").init()/>
<cfset resultQueue = asSearchObj.doSearch(ldapConnection,arguments.start,arguments.filter,attributeArray)/>
I also had to hack out a way to get results out of the queue, and this is what I came up with:
<cfset message =createobject("java", "com.novell.ldap.LDAPMessage")/>
<cfset continue = true/>
<cfloop condition="#continue#">
<cftry>
<cfset message = resultQueue.getResponse()/>
<cfset test = message/>
<cfcatch>
<cfbreak>
</cfcatch>
</cftry>
<cfswitch expression="#message.getClass().getName()#">
<cfcase value="com.novell.ldap.LDAPSearchResult">
<h1>got a result!</h1>
</cfcase>
<cfcase value="com.novell.ldap.LDAPSearchResultReference">
<!--- deal with these later --->
</cfcase>
<cfcase value="com.novell.ldap.LDAPResponse">
<!--- deal with these later --->
<cfdump var="#message.getResultCode()#"/>
</cfcase>
</cfswitch>
<cfflush>
</cfloop>
From what I've read, resultQueue.getResponse() blocks until something has come back... so this loop with iterate as long as the queue has stuff in it (and wait if it hasn't gotten anything). resultQueue.getResponse() will return null if there are no more messages in the queue, hence the need for the try/catch/break at the top. Each message pulled out of the queue is either a referral (not messing with these yet), a result (which contains an entry), or a response, which will indicate something about the search (like success/complete or error). I'm just researching this stuff at this point, but it could lead to more responsive UIs during long-running directory searches (if used with CFFLUSH).
| ||
| ||
|
| ||
| ||
|
| ||
| ||
| ||
| ||
| ||
| ||