How to Extend the NetWare Scripting Environment by Creating UCX Components
Articles and Tips: article
Manager Software Engineering
Novell Bangalore
msrivathsa@novell.com
01 Mar 2002
This AppNote showcases the creation of a Universal Component Extensions (UCX) component to access Lightweight Directory Access Protocol (LDAP) services, and demonstrates the usage of this component from the scripting languages.
- Introduction
- Component Development
- Converting the Design to Code
- Retrieving Arguments and Creating Return Values
- Component Usage
- Conclusion
Topics |
NetWare 6, scripting, Novell Script for NetWare (NSN), Perl, Universal Component eXtensions (UCX), Universal Component System (UCS) |
Products |
NetWare 6 |
Audience |
developers |
Level |
intermediate |
Prerequisite Skills |
familiarity with scripting, and NLM programming |
Operating System |
NetWare 6 |
Tools |
Novell Script for NetWare, NLM development tool such as CodeWarrior or Watcom, LDAP SDK for C |
Sample Code |
Yes |
Introduction
Scripting products such as Novell Script for NetWare (NSN), Perl, Universal Component eXtensions (UCX), and Universal Component System (UCS) are part of NetWare 6. These products help in easy and quick application development on NetWare. The UCX components shipped with NSN which exposes some of the common NetWare services and these can be used from scripting languages. The capabilities of the scripting infrastructure are not limited. It can be extended to provide access to more services. You don't have to wait for Novell to provide new components to satisfy your requirements. Instead, you can create your own components by making use of the UCX SDK. You can also distribute your component. This article showcases the creation of an UCX component to access LDAP services and the usage of this component from the scripting languages.
Software Requirements
The following software is required for creation of the UCX component demonstrated in this article. The documentation for these products is available at the respective URL.
Novell Script for NetWare available at http://developer.novell.com/ndk/nscript.htm. This contains the UCX SDK and the documentation.
NLM development tools such as CodeWarrior or Watcom.
LDAP SDK for C - NetWare and Windows available at http://developer.novell.com/ndk/cldap.htm.
Component requirements
The first step in component creation is the proper identification of the problem to be solved or the service to be exposed. Then identify the specific features, which are necessary in the component.
For our example we want to create an UCX component for accessing LDAP services that can be used for searching the LDAP directories.
Component Development
Component development starts with the proper design. Design of the component should be such that the component is easily usable and follows the standard design conventions used for the creation of components such as Java Beans, ActiveX controls, other UCX components. External interface of the component should be easy to use and the complexities of programming to the underlying service should be hidden inside the component. This can be achieved by identification of properties, methods, events, and relationship between the objects if there are sub objects in the component.
Now let us consider some of the features required in our component for LDAP services. We will name the component as UCX:LDAP. To identify an LDAP server, the component should have a server name and server port properties. Login and Logout methods are required to bind and unbind from the LDAP server. A search method is required to search the directory. The ability to search based on the search filter is an additional feature. To represent the search result, the component should support a collection of entry objects where each entry represents one object in the LDAP directory.
Based on this we can arrive at the simple object diagram below.
Object Diagram of LDAP Component.
The design of the component can be explained as shown below.
LDAP component design. Component Name: UCX:LDAP This component exposes the following constants: Search Scope constants: Constant Name Constant Type Constant Value LDAP_SCOPE_BASE Integer 0 LDAP_SCOPE_ONELEVEL Integer 1 LDAP_SCOPE_SUBTREE Integer 2 Properties: String Server Read/Write: Represents the name of the LDAP server. Default Value: "" Long Port Read/Write: Represents the LDAP server port. Default value 389. String LoginName Read Only: The name of the user who is logged in. Default Value: "" Methods: Bool Login(String username, String password) This method logs into the LDAP server and sets the LoginName property. In case of error sets the global error object and returns false. Bool Logout() Logs out of the LDAP server. Entries Search (String SearchFilter, String SearchBase, Integer SearchScope) SearchFilter: Standard LDAP search filter expression. SearchBase: The base context for search. SearchScope: The scope of search. Can be one of the search scope constants. Searches the directory and returns a collection of entry objects. In case of error, returns NULL and sets the err object. LDAPEntries object: Properties: Long count Read Only: Number of objects in the collection. Methods: Void Reset():Resets the collection. Entry Next(): Fetches the next element from the collection. Bool HasMoreElements(): Returns true if the collection contains some more elements. LDAPEntry Object: Properties: String FullName Read Only/Default Property: Fully distinguished name of the LDAP object. Methods: Nil
The methods Reset, Next, and HasMoreElements in the LDAPEntries object ensure the easy navigation through the collection using the For Each <Object> in <Collection> syntax of NSN.
Converting the Design to Code
The coding involves the declaration of constants, properties, methods, and the library procedure for each object.Then the actions specific to the property or method have to be incorporated.
Declare the Methods
For each method declare the method signature consisting of the return value and arguments as explained in the UCX SDK documentation. Then create an UCXMethod list. Ensure that the entries are arranged in the ascending order of their name in the method list.
The following code sample shows the method declarations for the LDAP object.
// Parameter description : UCX:LDAP Methods UCXParameter LDAPLoginParams[] = { {UCX_BOOLEAN_TYPE, "result"}, {UCX_STRING_TYPE, "userName"}, {UCX_OPTIONAL_TYPE, "password"}, {NULL, NULL} }; UCXParameter LDAPLogoutParams[] = { {UCX_BOOLEAN_TYPE, "result"}, {NULL, NULL} }; UCXParameter LDAPSearchParams[] = { {UCX_OBJECT_TYPE, "entries"}, {UCX_STRING_TYPE, "searchFilter"}, {UCX_STRING_TYPE, "searchBase"}, {UCX_LONG_TYPE, "searchScope"}, {NULL, NULL} }; enum { LDAP_LOGIN, LDAP_LOGOUT, LDAP_SEARCH, }; // Methods UCXMethod LDAPMethods[] = { {"LOGIN", LDAP_LOGIN, LDAPLoginParams}, {"LOGOUT", LDAP_LOGOUT, LDAPLogoutParams}, {"SEARCH", LDAP_SEARCH, LDAPSearchParams}, {NULL, NULL, NULL} };
Declare the Properties
Declare the properties in the property list. Each entry in the property list contains the name, data type and the visibility of the property. These lists are always terminated with a null element. The properties declared with visibility UCX_PRIVATE are internal to the component and cannot be accessed from the scripts. These can be used to store the private information.
The following code sample shows the property declaration for LDAP object. To preserve the LDAP handle we can make use of a private property LDAPHANDLE. The handle can be retrieved from this property and can be used in subsequent LDAP calls.
// Properties UCXProperty LDAPPropertyList[] = { {UCX_POINTER_TYPE, "LDAPHANDLE", UCX_PRIVATE}, {UCX_STRING_TYPE, "SERVER", UCX_PUBLIC}, {UCX_STRING_TYPE, "PORT", UCX_PUBLIC}, {UCX_STRING_TYPE, "LOGINNAME", UCX_PUBLIC}, {NULL, NULL, NULL} };
Constant Declaration
Components can expose constants. These constants can be directly used from the scripting languages. The following are the search scope constants of our component.
// Constants UCXConstant LDAPScopeConstants[] = { {UCX_INTEGER_TYPE, "LDAP_SCOPE_BASE", "0"}, {UCX_INTEGER_TYPE, "LDAP_SCOPE_ONELEVEL", "1"}, {UCX_INTEGER_TYPE, "LDAP_SCOPE_SUBTREE", "2"}, {NULL, NULL, NULL} };
Complete the above steps for each object. The following list contains all these declarations.
// LDAPEntries UCXParameter LDAPEntriesResetParams[] = { {UCX_BOOLEAN_TYPE, "void"}, {NULL, NULL} }; UCXParameter LDAPEntriesNextParams[] = { {UCX_OBJECT_TYPE, "entry"}, {NULL, NULL} }; UCXParameter LDAPEntriesHasMoreElementsParams[] = { {UCX_BOOLEAN_TYPE, "void"}, {NULL, NULL} }; enum { ENTRIES_HASMORELEMENTS, ENTRIES_NEXT, ENTRIES_RESET }; UCXMethod LDAPEntriesMethods[] = { {"HASMOREELEMENTS", ENTRIES_HASMORELEMENTS, LDAPEntriesHasMoreElementsParams}, {"NEXT", ENTRIES_NEXT, LDAPEntriesNextParams}, {"RESET", ENTRIES_RESET, LDAPEntriesResetParams}, {NULL, NULL, NULL} }; UCXProperty LDAPEntriesPropertyList[] = { {UCX_POINTER_TYPE, "SEARCHHANDLE", UCX_PRIVATE}, {UCX_LONG_TYPE, "COUNT", UCX_PUBLIC}, {NULL, NULL, NULL} }; // LDAP Entry UCXProperty LDAPEntryPropertyList[] = { {UCX_STRING_TYPE, "FULLNAME", UCX_PUBLIC}, {NULL, NULL, NULL} }; // Entries private structure to store search handle typedef struct tagUCXENTRIES{ LDAP *ld; LDAPMessage *searchResult; LDAPMessage *lastEntry; LDAPMessage *nextEntry; } UCXENTRIES, *PUCXENTRIES;
The next step is to combine all these elements together to create the definition for all the classes. You can pass NULL for those fields, which are not required for the component.
The following code sample shows the class list structure of the LDAP component.
// LDAP class definition static UCXClass LDAPClassList[] = { {"UCX:LDAP", 0, 0, 0, 0, &MyLib, LDAPMethods, NULL, LDAPPropertyList, UCXLDAPLibProc,NULL}, {"UCX:LDAP.LDAPENTRIES", 0, 0, 0, 0, &MyLib, LDAPEntriesMethods, NULL, LDAPEntriesPropertyList, UCXLDAPEntriesLibProc,NULL}, {"UCX:LDAP.LDAPENTRY", 0, 0, 0, 0, &MyLib, NULL, NULL, LDAPEntryPropertyList, UCXLDAPEntryLibProc,"FULLNAME"}, {NULL} };
Now include standard UCX macros, and a main function which registers the component.
// Standard macros UCXSTDPROCS(); UCXDEFINELIB(); ... UCXENTRY(NWDIR) { UCXINITLIBRARY2(LDAPClassList, "LDAP", "LDAP UCX Component", "-------------------", "Copyright 2001-2002 Novell Inc., All Rights Reserved"); UCXREGISTERLIBRARY(); } UCXUNLOADERPROC();
Once you are ready with the property/method declaration, the next step is to include the necessary code, which gets executed whenever the property/method is invoked. This is done through the library procedure.
How Library Procedure Works
The communication between the UCX Library Manager and the component is through predefined UCX events. Whenever a property or method is invoked, UCX library manager sends a relevant event to the library procedure. Handle this event to service a particular call. Some of the common event handling activities, which are applicable to most of the components, are:
UCX_EVENT_OBJECT_CREATE for allocation of any private data, initialization of the properties to the default values. UCX_EVENT_OBJECT_DESTROY for performing necessary cleanup.These events can be compared to the constructor and destructor of objected oriented paradigm.
UCX_EVENT_PROPERTY_GET and UCX_EVENT_PROPERTY_SET for retrieving and setting the property values.
UCX_EVENT_EXEC for servicing a particular method invoke request.
Dispatching of any unhandled events to the default event handler, UCXDispatchEvent.
The following code sample shows the library procedure for the LDAP component.
UCX_API UCXLDAPLibProc(void *CP, void *params, CHAR_PTR FN, uint32 Event, UCXClassLink *ClassLink, void *Object) { int index=0; LDAP *ld=NULL; switch( Event ) { case UCX_EVENT_OBJECT_CREATE: UCXSetIntegerProperty (CP, Object, "PORT", 389); UCXSetPointerProperty (CP, Object,"LDAPHANDLE", NULL); return UCXCreateBoolean(CP, TRUE,FN); break; case UCX_EVENT_OBJECT_DESTROY: ld = (LDAP *)UCXGetPointerProperty (CP, Object, "LDAPHANDLE"); if (ld != NULL){ ldap_unbind_s( ld ); } break; case UCX_EVENT_CONSTANT: return LDAPScopeConstants; case UCX_EVENT_PROPERTY_GET: break; case UCX_EVENT_PROPERTY_SET: if ((stricmp((char const *)FN,"LOGINNAME") == 0)){ UCXSetErrorText(CP, -300, FN, "Property is Readonly"); return NULL; } break; case UCX_EVENT_EXEC: index = UCXMethodIndex(CP, ClassLink->Class, FN); if( index == -1 ) /* didn't find the function */ break; switch(index) { case LDAP_LOGIN: return Login(CP,FN,params,Object); case LDAP_LOGOUT: ld = (LDAP *)UCXGetPointerProperty (CP, Object, "LDAPHANDLE"); if (ld != NULL){ ldap_unbind_s( ld ); UCXSetPointerProperty (CP, Object,"LDAPHANDLE", NULL); } return UCXCreateBoolean(CP,TRUE,FN); case LDAP_SEARCH: return Search(CP,FN,params,Object); } } // Dispatch Events that are not used return(UCXDispatchEvent(CP, params, FN, Event, ClassLink, Object)); }
Retrieving Arguments and Creating Return Values
Arguments for the methods or the assignment values to the properties can be extracted using the UCXGet<Type>Param functions. For example UCXGetStringParam can be used to retrieve the string argument. Component return values have to be of UCX data types, and can be created using the UCX create functions.For example, UCXCreateBoolean can be used for creation of a boolean type. The following code sample shows all of these.
UCX_API Login(void *CP, CHAR_PTR FN, void *Params, void * Object) { char *pszName=NULL, *pszPassword="", *pszHost=NULL; int ldapPort=389,version,rc=0; LDAP *ld=NULL; pszName = UCXGetStringParam(CP, Params,1); pszPassword = UCXGetStringParam(CP, Params,2); pszHost = UCXGetStringProperty (CP, Object, "SERVER"); UCXGetIntegerProperty (CP, Object, "PORT", &ldapPort); /* Set LDAP version to 3 */ version = LDAP_VERSION3; ldap_set_option( NULL, LDAP_OPT_PROTOCOL_VERSION, &version); /* Initialize the LDAP session */ if (( ld = ldap_init( pszHost, ldapPort )) == NULL) { UCXSetErrorText(CP, -1, FN, "LDAP Initialization Failed"); return UCXCreateBoolean(CP,FALSE,FN); } /* Bind to the server */ rc = ldap_simple_bind_s( ld, pszName, pszPassword ); if (rc != LDAP_SUCCESS ){ UCXSetErrorText(CP, rc, FN, ldap_err2string(rc)); ldap_unbind_s ( ld ); return UCXCreateBoolean(CP,FALSE,FN); } UCXSetPointerProperty (CP, Object,"LDAPHANDLE", ld); UCXSetStringProperty (CP, Object,"LOGINNAME", pszName); return UCXCreateBoolean(CP,TRUE,FN); }
Error Handling
The general convention is to set the run time error in a global error object. It is up to the script to either stop the script execution for the runtime errors or to resume the script execution, ignoring the error. The error information can be set using the UCXSetErrorText function.To complete the component development, include the library procedures for all the classes. The following includes the remaining code required for the completion of the LDAP component.
UCX_API Search(void *CP, CHAR_PTR FN, void *Params, void * Object) { char *pszFilter=NULL, *pszBase=NULL; int searchScope=0, rc=0; LDAP *ld=NULL; LDAPMessage *searchResult=NULL; void *subObj=NULL; void *pEntriesParams=NULL; char *attrs[]={ LDAP_NO_ATTRS, NULL }; PUCXENTRIES pEntries=NULL; pszFilter = UCXGetStringParam(CP, Params,1); pszBase = UCXGetStringParam(CP, Params,2); searchScope = UCXGetIntegerParam(CP, Params,3); ld = (LDAP *)UCXGetPointerProperty (CP, Object, "LDAPHANDLE"); /* Search the directory */ rc = ldap_search_ext_s ( ld, /* LDAP session handle */ pszBase, /* container to search */ searchScope, /* search scope */ pszFilter, /* search filter */ attrs, /* Attributes*/ 0, /* return attributes and values */ NULL, /* server controls */ NULL, /* client controls */ LDAP_NO_LIMIT, /* time out */ LDAP_NO_LIMIT, /* no size limit */ &searchResult ); /* returned results */ if ( rc != LDAP_SUCCESS ){ UCXSetErrorText(CP, -3, FN, ldap_err2string( rc )); return UCXCreateBoolean(CP,FALSE,FN); } pEntries = (PUCXENTRIES)malloc (sizeof (UCXENTRIES)); if (pEntries){ pEntries->ld = ld; pEntries->searchResult = searchResult; } else{ UCXSetErrorText(CP, ERR_OUT_OF_MEMORY, FN, "Server out of memory"); return NULL; } pEntriesParams = UCXCreateParam (CP, FN, UCX_LONG_TYPE, pEntries, NULL,NULL); if( (subObj = UCXInstantiateObject(CP, "UCX:LDAP.LDAPEntries", FN, pEntriesParams )) == NULL ) UCXSetErrorText(CP, ERR_INSTANTIATION, FN, "Error Instantiating Entries collection"); UCXDestroyParam(CP,pEntriesParams); return subObj; } static void Reset(void *CP, CHAR_PTR FN, PUCXENTRIES pEntries); static UCX_API Next(void *CP, CHAR_PTR FN, PUCXENTRIES pEntries); static UCX_API HasMoreElements(void *CP, CHAR_PTR FN, PUCXENTRIES pEntries); UCX_API UCXLDAPEntriesLibProc(void *CP, void *params, CHAR_PTR FN, uint32 Event, UCXClassLink *ClassLink, void *Object) { PUCXENTRIES pEntries=NULL; int index=0; switch( Event ) { case UCX_EVENT_OBJECT_CREATE: { unsigned long entryCount=0; pEntries=(PUCXENTRIES)UCXGetLongParam (CP,params,1); if (pEntries == NULL) return UCXCreateBoolean(CP, FALSE, FN); pEntries->lastEntry = NULL; pEntries->nextEntry = NULL; UCXSetPointerProperty (CP, Object,"SEARCHHANDLE", pEntries); entryCount = ldap_count_entries( pEntries->ld, pEntries->searchResult ); UCXSetLongProperty(CP,Object, "COUNT", entryCount); return UCXCreateBoolean(CP, TRUE, FN); } case UCX_EVENT_OBJECT_DESTROY: if( (pEntries = (PUCXENTRIES) UCXGetPointerProperty(CP, Object, "SEARCHHANDLE")) != NULL ){ ldap_msgfree( pEntries->searchResult); free (pEntries); } break; case UCX_EVENT_PROPERTY_GET: break; case UCX_EVENT_PROPERTY_SET: if (stricmp((char const *)FN,"COUNT") == 0) return NULL; break; case UCX_EVENT_EXEC: // function dispatch from lib index = UCXMethodIndex(CP, ClassLink->Class, FN); if( index == -1 ) /* didn't find the function */ break; if( (pEntries = (PUCXENTRIES) UCXGetPointerProperty(CP, Object, "SEARCHHANDLE")) == NULL ) return UCXCreateBoolean(CP, FALSE, FN); switch (index) { case ENTRIES_HASMORELEMENTS: return HasMoreElements (CP,FN,pEntries); case ENTRIES_NEXT: return Next (CP,FN,pEntries); case ENTRIES_RESET: Reset (CP,FN,pEntries); return UCXCreateBoolean(CP, TRUE, FN); } } // Dispatch Events that are not used return(UCXDispatchEvent(CP, params, FN, Event, ClassLink, Object)); } void Reset(void *CP, CHAR_PTR FN, PUCXENTRIES pEntries) { pEntries->lastEntry = NULL; pEntries->nextEntry = NULL; return; } UCX_API Next(void *CP, CHAR_PTR FN, PUCXENTRIES pEntries) { char *pszObjectName = NULL; void *subObj = NULL; if ( pEntries->nextEntry == NULL){ if (pEntries->lastEntry == NULL) pEntries->lastEntry = ldap_first_entry( pEntries->ld, pEntries->searchResult ); else pEntries->lastEntry = ldap_next_entry( pEntries->ld, pEntries->lastEntry); } else{ pEntries->lastEntry = pEntries->nextEntry; pEntries->nextEntry = NULL; } if (pEntries->lastEntry == NULL){ UCXSetErrorText(CP, -100, FN, "No more entries"); Return NULL; } else{ if( (subObj = UCXInstantiateObject(CP, "UCX:LDAP.LDAPENTRY", FN, NULL )) == NULL ) UCXSetErrorText(CP, ERR_INSTANTIATION, FN, "Error Instantiating LDAPENTRY"); else{ pszObjectName = ldap_get_dn( pEntries->ld, pEntries->lastEntry ); if (pszObjectName){ UCXSetStringProperty (CP, subObj, "FULLNAME",pszObjectName); ldap_memfree( pszObjectName ); } } return subObj; } } UCX_API HasMoreElements(void *CP, CHAR_PTR FN, PUCXENTRIES pEntries) { if (pEntries->lastEntry == NULL) pEntries->nextEntry = ldap_first_entry( pEntries->ld, pEntries->searchResult ); else pEntries->nextEntry = ldap_next_entry( pEntries->ld, pEntries->lastEntry); if (pEntries->nextEntry) return UCXCreateBoolean(CP, TRUE, FN); else return UCXCreateBoolean(CP, FALSE, FN); } UCX_API UCXLDAPEntryLibProc(void *CP, void *params, CHAR_PTR FN, uint32 Event, UCXClassLink *ClassLink, void *Object) { switch( Event ) { case UCX_EVENT_OBJECT_CREATE: return UCXCreateBoolean(CP, TRUE, FN); case UCX_EVENT_PROPERTY_SET: if ( (stricmp((char const *)FN,"FULLNAME") == 0)){ UCXSetErrorText(CP, -300, FN, "Property is Readonly"); return NULL; } break; } // Dispatch Events that are not used return(UCXDispatchEvent(CP, params, FN, Event, ClassLink, Object)); }
The Final Steps
Compile the source code and link it with relevant libraries to build an NLM.
Copy the NLM to the NetWare server and edit the SYS:UCS\UCX.INI file to set the path for the NLM. For example, UCX:LDAP=SYS:UCS\UCX\LDAP.NLM.
Component Usage
The following NSN script demonstrates the usage of this component to list the entries from an LDAP directory.
'Create an LDAP object, set the address and search for all the objects ' Set ldap=createobject("UCX:LDAP") container="ou=msrivathsa,ou=user,o=novell" ldap.server="nldap.com" ldap.port=389 ldap.login ("cn=admin,ou=msrivathsa,ou=user,o=novell", "novell") Set entries=ldap.search("(objectclass=*)", container, 1) Print "Total Entries Found: " &CStr(entries.count) For Each entry In entries Print entry.fullname Next ldap.logout
Conclusion
The scripting infrastructure on NetWare helps in easy application development. It can be extended by developing UCX components for various services. UCX component is an encapsulated and abstracted unit of functionality, which exposes services in the form of properties, methods, and events. UCX components can be created using UCX SDK which is part of Novell Script for NetWare.
* Originally published in Novell AppNotes
Disclaimer
The origin of this information may be internal or external to Novell. While Novell makes all reasonable efforts to verify this information, Novell does not make explicit or implied claims to its validity.