"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;
/**
* @author
RossD
*/
import
com.novell.ldap.*;

public
class ldapSearchAS {
   
public
LDAPSearchQueue doSearch(LDAPConnection lc, String searchBase, String searchFilter, String[] attrs)
        
throws
com.novell.ldap.LDAPException
         {
          LDAPSearchQueue queue =
             
lc.search( searchBase,
// container to search
                            
LDAPConnection.SCOPE_ONE,
// search container only
                           
searchFilter,
// search filter
                           
attrs,
// return cn and sn
                           
false
, // return attrs and values
                           
(LDAPSearchQueue)null
, // use default search queue
                            (LDAPSearchConstraints)null
);
             
return
queue;
         }
}

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).


09/09/2004 02:36 P - CL said...

Dave, do you know if there is a way to get a count of entries in a tree based on a filter without actually pulling back an LDAPSearchResult?

Thanks!


09/09/2004 03:53 P - Dave Ross said...
hmm... I don't think so. Are you having scalability issues?

09/09/2004 06:03 P - CL said...

I haven't tested for scalability yet, it just seems like such a waste to bring back all that data.

The situation is like this. I have a location tree, and each entry in the location tree has alias entries to entries in a person tree. I'd like to be able to generate a list of locations with a count value for the number of alias references.

I've got over 100k entries in my person tree, so you can see why I'm not crazy about pulling that data back unnecessarily just to get a count.


09/09/2004 06:19 P - Dave Ross said...

CL,

Sorry about the comment weirdness. I realized that the body property was set to string instead of longchar (thus the associated db field wasn't long enough)

Anyways, to answer your question, I think if you limit your search to just DNs it wouldn't be all that much data.

I'll admit I don't use directories for anything other than authentication and authorization, but I think that is going to change in the near future. I plan on doing a full write-up of JLDAP and CFMX in the near future as I move forward with this stuff.


11/26/2004 07:54 A - IainR said...

Hi Dave,
You said in your last post you planned to do a write-up of JLDAP & CFMX...  Have you had a chance to do it? 

I'm looking for a tool other than CFLDAP for my user creation scripts.
Does jldap work against all directories or only Novell?

Thanks
IainR


11/26/2004 09:05 A - Dave Ross said...

yes, JLDAP works with any LDAPv3 compliant directory.

I probably should do a full write-up of JLDAP & CFMX, but I have been directing people with questions to the 3 existing blog entries. Is there anything specific you need help with?


11/26/2004 09:13 A - IainR said...

Thanks for getting back so quickly!

CFLDAP in CF5 doesn't seem to handle multiple master servers hidden behind readonlys... My full explanation is on the CF forums:
http://www.macromedia.com/cfusion/webforums/forum/messageview.cfm?catid=7&threadid=925506&highlight_key=y&keyword1=ldap#3303578

We don't know if this is still an issue in CFMX (I need to prepare some tests), but we still need to look at alternative options.


11/26/2004 09:39 A - Dave Ross said...

The problem with MX caching the dns lookup can be fixed by setting network.cache.ttl equal to something small.

I'm not sure if you would continue to have the problems CFMX, but if you did JLDAP or JNDI would probably do the trick.


09/28/2007 08:56 P - Billy said...

Thnax on that,I have here some update on CMFX and Apache,hope this would serve to be helpful..=)

CFMX on IIS and Apache2 on the same machine

Well with a new machine in the works for my company i've been thinking about running Apahce (on windows) for our server. Anyway what with mappings etc i really wanted to give this a bash at home first. At first i thought this was gonna mean me, notepad and a bunch of text files had to get friendly. Actually its couldn't of been easier thanks to CF.

  • Download and install Apache. I needed to change the port to 81 so it didnt interfere with IIS which meant i changed the "Listen" port to 81 in the httpd.conf file
  • Edit C:\CFusionMX\bin\connectors\Apache_connector.bat to look at "apache2" folder instead of "Apache". Then run the bat file.
  • Restart Apache and all is well, easy.


Do remember though that they are using the same CF engine. If you have the enterprise version you could most likely set up a different instance for each web server, but i dont so i can't ;o(

Afterthought:If you want to administer CF from Apache then you will need to add the following to the Mazda tailgate handle-ScriptAlias section of httpd.cong


10/08/2009 06:02 A - Reil said...
"That's right," the man said. "I couldn't remember the word." He was the only t, then high school students, and, finally, to anyone aged 13 and over. The website currently has more than 175 million active users in amount of visitors, making Facebook the most popular social network, followed by MySpace and Twitter.other human at the loading dock this morning. The man didn't have a name, just a number, like the rest of the robots. Paris, at Night.

Post a comment:

(required, will not be displayed)
 


   You will be sent an email asking you to validate your comment.



Driven by Farcry Open Source CMS. Dressed in Aura.
Powered by ColdFusion MX.