Skip to main content

skip to main content

developerWorks  >  Information Management | WebSphere  >

Integrate unstructured content into a distributed federated system, Part 2: Develop a connector for WebSphere Information Integrator Content Edition

Writing your connector

developerWorks
Document options

Document options requiring JavaScript are not displayed

Sample code


Rate this page

Help us improve this content


Level: Intermediate

Ryan Graciano (graciano@us.ibm.com), Software Engineer, IBM

19 Jan 2006

This series of articles takes you through the development of a new connector for WebSphere® Information Integrator Content Edition. The first article in the series discussed connector development planning, which spent some time mapping the content repository's concepts to the II CE connector. This article will bring those concepts to life with actual connector code. A connector to the WebSphere Information Integrator OmniFind Edition 8.2.1 repository, which was discussed in the last article, will be used here as an example.
OmniFind, Version 8.2.1
Note that later versions of OmniFind Edition have been released since 8.2.1, and improvements have been made. Modifying or upgrading the sample connector to better fit the later versions of OmniFind is left as an exercise for the reader.

Background

WebSphere Information Integrator Content Edition (II CE) provides a single interface to any number of content repositories and workflow systems. This single interface makes it easy for application developers to integrate all of their information sources into new or existing enterprise applications.

Repositories are accessed by II CE with connectors that are deployed to a J2EE application server, such as WebSphere Application Server. II CE includes many connectors, but you might need one that's not yet available. In this case, a connector SDK gives developers the freedom to integrate additional repositories into II CE's federated system. With the SDK, new content repositories and workflow systems can be seamlessly added to enterprise applications that leverage II CE.



Back to top


Starting your connector

Extending BaseBridge

When you envision connector development, you probably think about mapping specific functions, such as creating or deleting content in the native repository. You may even have some prototyping code that you would like to migrate into an II CE connector, but you're not yet sure where or how to implement it. II CE provides a superclass called BaseBridge that serves as a hub for connector development and is the best starting point for developers.

Your first act as a connector developer should be to create a class, typically called ConnectorBridge.java, where Connector is the name of your connector. There are other supporting classes that you will need to define, but you'll create those later on. Your ConnectorBridge class should implement the java.io.Serializable interface, it should be serializable, and it should extend BaseBridge. BaseBridge implements the IBridge interface, which is required of all II CE connectors.

BaseBridge, an abstract class, will contain all of the methods that you could conceivably implement in your connector. Most of these methods, however, are not abstract. If they are not overridden, they will throw UnsupportedActionException or provide another standard default behavior, such as returning an empty array or null. This makes it easy for the developer to selectively add or remove functionality from the connector. Furthermore, the BaseBridge superclass guarantees forward compatibility of your connector to future versions of II CE. Although it's possible to create a connector that implements IBridge directly, it's strongly recommended that you extend BaseBridge to preserve that forward compatibility.



Back to top


Logging on

The first methods that need to be implemented in any connector are the logon(AuthBundle) and logoff() functions. You'll recall from the last article in this series that the connector needs to maintain a continuous session for the user, so your logon method needs to store a reference to a session object from your repository. If your repository does not support stateful sessions, then your connector must store the credentials it receives from the AuthBundle to authenticate future requests. AuthBundles can either be encrypted (sealed) or plain-text (unsealed). For AuthBundles that are encrypted, the BaseBridge class provides a protected void unsealAuthBundle(AuthBundle) method. This method can also accept unsealed AuthBundles (it will just ignore them), so you could simply have your logon method run all AuthBundles through unsealAuthBundle. Alternatively, you could check AuthBundle::isSealed() before trying to unseal the bundle.

Before you unseal your bundle in logon(AuthBundle), remember that you never want to store unsealed AuthBundles as instance variables in your connector. As we discussed in the previous article, unsealed AuthBundles may be vulnerable during serialization. Instead, you should create a copy of your encrypted AuthBundle with the public AuthBundle::clone() method, and pass the copy of the AuthBundle to the unsealAuthBundle method.


Listing 1. Copying and unsealing the AuthBundle
			
protected AuthBundle copyAndUnsealAuthBundle(AuthBundle bundle )
throws NotLoggedOnException {
  try {
    AuthBundle unsealed = (AuthBundle)bundle.clone();
    unsealAuthBundle( unsealed );
    return unsealed;
  } catch( CloneNotSupportedException cnse ) {
    throw new NotLoggedOnException( "Could not clone the authentication bundle",
    cnse );
  } catch( LogonException le ) {
    throw new NotLoggedOnException( "Could not unseal the authentication bundle", 
    le );
  }
}

Your logon method also needs to verify that the logon credentials are correct. For repositories that do not support a continuous session, this can be a non-obvious addition because your logon is probably not doing much more than storing the encrypted AuthBundle for future use. However, it's desirable to tell the user that their credentials do not work ahead of time, throwing a LogonException during the actual logon procedure. This is when a client will be expecting a logon failure, and it will be easier for the end user to understand and diagnose a credential problem during this time.

The OmniFind example connector includes a method called checkOECredentials that will execute a very simple query with a maximum of one result. This method is called during logon and in the isLoggedOn method to determine whether or not the credentials are valid. If an exception is thrown by the OmniFind Search and Index API (SIAPI) during the query, then that exception is wrapped in a LogonException and returned to the user. Thus, the user is immediately made aware that their credentials are invalid.


Listing 2. Verifying credentials in the example connector to OmniFind
			
protected void checkOECredentials( AuthBundle bundle ) throws LogonException {
  if( bundle == null ) {
    logError( "The connector does not currently have any stored credentials." );
    throw new LogonException( "Unable to logon to connector; no credentials
    are established" );
  }

  try {
    OmniFindSearch oesearch = new OmniFindSearch( _config, copyAndUnsealAuthBundle(
    bundle ) );
    oesearch.executeOEQuery( "1", 1 );
  } catch( Exception e ) {
    logDebug( e );
    throw new LogonException( "Unable to logon to connector", e );
  }
}

Connector logging
II CE uses log4j for logging. Inside the BaseBridge class, you'll find several convenience methods for logging, such as logDebug, logError, and logWarning.

Repository credentials

This is a good time to note that your repository may have multiple forms of authentication, so you may need to decide how to map the different credentials in your connector's logon method. The AuthBundle provides three different fields that can be transferred from the client to the connector: username, password, and optional. Most connectors don't require the optional parameter, but it can be useful if your repository requires that you qualify your credentials, such as with a Windows NT domain or a user role. optional can also be used if your repository supports something other than a user id/password pair, such as a single sign-on or biometric token.


Listing 3. Retrieving credentials from the AuthBundle and using them in the OmniFind SIAPI
				
protected ApplicationInfo _appinfo = null; // Declaring the SIAPI ApplicationInfo
_appinfo = _factory.createApplicationInfo( authbundle.getUsername() );
// authbundle is an unsealed AuthBundle
_appinfo.setPassword( authbundle.getPassword() );

It's not recommended that your connector use the optional parameter to control connector behavior. For example, it would not be advisable for the optional parameter to determine what the user's root folder in the repository will be. That model is detrimental to connector usability because it requires that the user possess a greater knowledge of repository internals than should be necessary to use the connector. The AuthBundle should be used solely for authentication, and all other configuration information should be set with the II CE Administration Tool.

OmniFind Authentication
OmniFind also supports more fine-grained authentication methods, which restrict user access on the document level. A desirable enhancement to the OmniFind connector would be to move the Application ID and password into the connector's configuration and to require specific user authentication on the connector's logon method. Note that future versions of OmniFind have deprecated the Data Listener and have moved its functionality into the core Search and Index API (SIAPI).

Example connector: OmniFind authentication

The example OmniFind connector uses a fairly basic authentication model. It uses application IDs as user credentials. Application IDs are provided by OmniFind as a simple method of authentication and can be configured in the Enterprise Search Administration application. Application IDs are also associated with application passwords, which are configured in a resource bundle in ESSearchApplication.war. Application IDs give the authenticated client application access to all documents in a certain collection. Logging on to the OmniFind connector requires that you use the Application ID and its corresponding password.


Figure 1. Logging into the OmniFind connector in the II CE Web Client
Logging into the OmniFind connector in the II CE Web Client

The OmniFind 8.2.1 example connector requires a second authentication, which is a complication that you may see in your own repository. The OmniFind Data Listener (DL) component, which is the API used for content creation and deletion functionality in the connector, needs to be logged into separately. The DL does not support fine-grained, per user access control. Instead, it allows users to add, delete, or replace any item.

The solution used by the example connector requires that a DL username and password be present in the connector configuration. This means that if the DL username and password are configured correctly on a particular connector, then any authenticated user has full access to the DL.

OmniFind collections
For our purposes, you can think of an OmniFind collection as a separately searched and accessed instance of the OmniFind repository.

If a user would like to restrict DL access, then multiple connectors could be set up. One connector would then configure empty DL credentials and an appropriate OmniFind collection, making it impossible for users of that collection to create and delete content. A second connector could be configured with an OmniFind collection that is very restrictive in its access, and that connector would include the proper DL credentials. Therefore, the second connector would support creating, updating, and deleting content in that restricted collection.



Back to top


Repository items in II CE

As mentioned in the last article, content retrieval and management is the end game in all of this, so item retrieval will probably take center stage in the connector development process. Before you begin writing the mapping code for items in your repository, you need a solid understanding of what an item is to II CE.

All items in II CE are descendants of the RepoItem class, which provides some generic functionality that applies to all items. Each RepoItem has a RepoItemHandle , which holds the ID of the item, the type of the item, the item's version, and the repository in which the item is held. RepoItems also know about the item's associated metadata because all items in II CE can be associated with properties, which are structured metadata about the content. Each Property has a name and a string value, but clients require more information. Clients to II CE will want to know things about the property, such as its type and whether or not it's queryable. This kind of information is stored in a PropertyDescription , which is attached to each Property.


Listing 4. Creating a Property and adding it to a RepoItem
				
PropertyDescription pd = new PropertyDescription( "PDExample", IDataType.STRING,
IDataType.EDITOR_TEXTBOX );
pd.setIsQueryable( false );

Property prop = new Property( pd );
prop.setValue( "my property value" );
prop.setIsChanged( false ); // when a property is updated, it's flagged as changed

Content item = new Content(); // Content is a kind of RepoItem
item.addProperty( prop );

Property descriptions are organized into item classes, which were discussed in some detail in the last article. Each item that your connector returns to the client will be associated with an ItemClass, even if that class is just the generic, global class. In some cases, there may be no item class on an item, so null can be returned.

Property descriptions that aren't part of an item class can still be used to create properties; it's not required that a property description be part of an item class. However, item classes make it easy to look up a property description when it's needed. If you create a PropertyDescription just for a particular Property and don't add it to any item class, then it can only be accessed through that Property.



Back to top


Item retrieval in your connector

Item retrieval is implemented by your connector in the public RepoItem getRepoItem(RepoItemHandle) method. Like almost all of the methods that you will override in BaseBridge, getRepoItem should begin by verifying that the connector instance is currently logged on. If it finds that it's not logged on, then a NotLoggedOnException should be thrown. The OmniFind connector calls a validateLogon() method at the beginning of each operation, which uses checkOECredentials() to verify that you have established valid credentials.

UnsupportedActionException
As a general rule of thumb, you will be throwing an UnsupportedActionException whenever the user tries to do something that your connector does not support. Your clients should be consulting the RepositoryProfile for your repository before they try to perform an unsupported action, so these exceptions should not often occur on the client side. This will be discussed later on, when we go over ConnectorBridgeFactory.java.

After the session is validated, your connector should determine the type of the item that is being requested. This is done by calling RepoItemHandle::getItemType() and comparing it to one of the constants in the IItemType interface. In many connectors, different item types will require different item retrieval code. The OmniFind connector only accepts IItemType.CONTENT and IItemType.FOLDER, and it throws an UnsupportedActionException when an unsupported item type is requested.

After it has determined the item type, your connector will need to construct the item in the appropriate class and return it to the user. The four item classes currently in II CE are Content, Folder, WorkQueue, and WorkItem. All of them extend RepoItem, so you can return any one of them from the getRepoItem method.


Listing 5. Example getRepoItem that only supports CONTENT
				
public RepoItem getRepoItem( RepoItemHandle handle ) {
  validateLogon();

  if( handle == null || handle.getItemID() == null ) {
    throw new ItemNotFoundException( "Can not retrieve items with a null
    RepoItemHandle or ItemID" );
  }

  if( handle.getItemType() == IItemType.CONTENT ) {
    Content content = new Content();
    // ... code to set item attributes and properties would go here ... //
    return content;
  }

  else {
    throw new UnsupportedActionException( "Try another kind of item!
    [Perhaps content?]" );
  }
}



Back to top


Example connector: Content retrieval

The next few sections of this article will take you through the rest of the process for content retrieval in the example connector. Although each section is specific to the OmniFind connector and is tailored to the OmniFind repository, many of the concepts will still apply to connector development in general. You'll learn about attributes and properties, both of which are used by almost every connector, and you'll get some ideas about how properties and property descriptions can be created, stored, and accessed by your connector. You'll also get a general overview of the content retrieval process, from Content object creation through native content and metadata.

Getting content from the repository

Connector method visibility
It's generally a good idea to mark internal methods as protected rather than private. This makes it considerably easier to subclass and modify your connector in the future.

When the getRepoItem method detects IItemType.CONTENT in the example connector, it dispatches the request to an internal method called protected Content getContentFromOE(String itemID). Here is where the actual wrapping code for the OmniFind repository begins to take shape. getContentFromOE begins by initiating an OmniFind query of the form docid:itemID and retrieves an OmniFind Result object. It will use this object to populate all of the metadata for the Content object.



Back to top


Example connector: Item attributes

First, the example connector sets the II CE metadata for the Content object, which I will refer to here as attributes. Attributes can be understood as the set of methods on the repository item that describe the item. For example, the RepoItemHandle, the name of the item, the creation date of the item, and anything else set on the actual Content object is an attribute. The Result object retrieved from OmniFind is used sparingly here, as many of the attributes apply to all Content objects for this repository. A good illustration of this is the setCanCheckout(boolean) method on Content; because the OmniFind connector does not support checking content out, it always sets this attribute to false. You are also setting the name of the item class at this point in time, which will be discussed when we talk about item properties.


Listing 6. Excerpt from example connector's setItemAttributes
				
protected void setItemAttributes( Content item, Result oeSearchResult,
String itemID ) {
  // Basic item information
  item.setHandle( new RepoItemHandle( _systemID, itemID, IItemType.CONTENT ) );
  item.setItemClassName( GLOBAL_CLASS );
  item.setName( oeSearchResult.getTitle() );

  // Basic actions
  item.setCanDelete( true );  //often per user, but not so here
  item.setCanRead( true );    //often per user, but not so here
  item.setCanUpdate( false ); //often per user, but not so here

Native content attributes

The OmniFind example connector does not support native content (binary data or files) because the repository does not store native content. Note that if it did, then native content would most likely be handled at this point, in the setItemAttributes method. It's important not to actually retrieve the native content at this point, however. Instead, find out how many pages of native content exist, as well as the content links, and set that information on the Content object. Clients to II CE can then request the native content from your repository as needed, but it will not be provided automatically. The benefit that from this model is a dramatic speed improvement on content retrieval, which allows the user to get a large number of content items quickly and efficiently.



Back to top


Example connector: Item properties

The next step in the construction of a RepoItem (in this case, Content) is the addition of item properties to the RepoItem. First, you need to understand the kinds of metadata that are available to you about a content item in the repository. Recall that the content in the example's repository is returned as an OmniFind Result object. From the OmniFind client's point of view, there are three kinds of metadata available for a Result:

  1. Getters on the Result, such as getTitle(), getScore(), and getLanguage()
  2. OmniFind Fields (analogous to II CE properties) that are available from getFields() on the Result object and exist globally in Searchable::getAvailableFields()
  3. Fields that are available from getFields() on the Result but are ad-hoc

OmniFind does not define any kind of schema for properties, so it does not support item classes. However, there are some properties that exist on all items, such as the getters on the Result object, and there are some globally-defined fields that may exist on our content. Each Content object will then be a member of the global item class, which will contain all of the afore-mentioned properties. It's important to note that not all of the properties in the item class will necessarily exist on every Content object in that class, and that is acceptable.

The rest of the properties on the Content object will be ad-hoc (not in any item class), according to what extra Fields exist for the Result.


Figure 2. Properties on an arbitrary item in the OmniFind connector
Properties on an arbitrary item in the OmniFind connector

Now that you know what your Property objects will be, you have to look at what II CE expects. You need not only a Property, but also a PropertyDescription for each Property that the connector defines. Choosing a PropertyDescription can be a little tricky because the Property objects all come from different places. To understand how the PropertyDescription should be built for each kind of Property, take a closer look at how these properties will be used in the connector, particularly in property-based search. Much of the property description describes search capabilities for the property, such as getSupportedOperators() and getIsQueryable().

System properties

The properties that are getters, not Fields, all have to be queried for in some special way from the OmniFind repository. For example, to search on getSource(), which the connector defines as the property named "Source," the connector will have to query for "$source::value," which is a syntax that is entirely different from a regular OmniFind property query. What's noticeable about this syntax is that there is no way to provide operators such as ">," "<," or "LIKE." You can only query for "$source::value" or "-$source::value," which would equate to "EQUALS" and "NOT EQUALS" in the II CE query language. Since these properties are set by the system on the Result object, and you have to give them special treatment, it makes sense to set them apart somehow from the other properties. Accordingly, the OmniFind connector uses PropertyDescription::setIsSystemProperty(true) on these properties, and it sets their operators to only support EQUALS and NOT EQUALS. For the system properties that cannot be searched for, such as "Score" (getScore() on Result), setIsQueryable(false) sets the supported operators to be the empty string.


Listing 7. Creating a system property description for a static list of properties
				
protected PropertyDescription[] buildSystemPropertyDescriptions() {
  PropertyDescription[] systempds = new PropertyDescription[5];
  // Note that although we have a defined number of system properties here, your
  // system properties do not have to be pre-determined. They could just as easily be
  // dynamically defined by your connector.

  // PROP_SOURCE is a String constant, "Source"
  systempds [0] = new PropertyDescription( PROP_SOURCE,
  IDataType.STRING, IDataType.EDITOR_TEXTBOX );
  systempds [0].setAllowsMultipleValues( false );
  systempds [0].setIsReadOnly( true );
  systempds [0].setIsQueryable( true );
  systempds [0].setIsSelectable( true );
  systempds [0].setIsSystemProperty( true );
  systempds [0].setLabel( PROP_SOURCE );
  systempds [0].setSupportedOperators( SUPPORTED_OPS_SYSTEM );

  // Abridged to only show relevant code

System property descriptions in the OmniFind connector are stored in a global item class. When the connector needs to add a system property to a Content object, it will first look up the property description from the global item class. The property description is used to create a Property object for the system property, and then the Property is added to the Content object.


Listing 8. Setting the system property to a value
				
protected void setItemProperties( Content item, Result searchResult ) {
  // The buildOEGlobalClass method calls the buildSystemPropertyDescriptions
  // method, as well as the other methods for building property descriptions, which
  // you will read about in the next section
  ItemClass globalclass = buildOEGlobalClass();

  // The item class returned by buildOEGlobalClass() is used to look up
  // the property description we built earlier. We then use that property description
  // to create a property
  Property prop = new Property( globalclass.getPropertyDescriptionByName(
  PROP_SOURCE ) );
  prop.setValue( searchResult.getSource() ); // Using the getter to set the value
  prop.setIsValueChanged( false );

  item.addProperty( prop );

  // Abridged to only show relevant code

Properties with property descriptions

Now the example connector handles the second kind of property, which consists of the globally-defined fields. These all have OmniFind FieldInfo objects associated with them, which give a window into what operators should be supported and whether or not that field should be queryable. There is a small risk associated with this, however. It's possible that some of the fields that look like globally-defined fields are actually ad-hoc fields that happen to share the same name. Unfortunately, there is no way to tell if this is occurring, so there is a small chance that the PropertyDescription for each of these fields is not 100% accurate. However, being that this is a rare case, and that without these globally-defined fields you would have to simply guess at property descriptions for all of the fields, we accept this risk and document it accordingly.


Listing 9. Building property descriptions dynamically from OmniFind
				
FieldInfo[] fields = search.getSearch().getAvailableFields();
PropertyDescription[] allpds = new PropertyDescription[ fields.length ];

for( int i = 0; i < fields.length; i++ ) {
  allpds[i] = new PropertyDescription( fields[i].getID(), IDataType.STRING,
  IDataType.EDITOR_TEXTBOX );
  allpds[i].setAllowsMultipleValues( false );
  allpds[i].setIsReadOnly( true );

  // Note that we check that the property can be queried *as a property*, not as
  // fulltext.
  if( fields[i].isFieldSearchable() ) {
    allpds[i].setIsQueryable( true );
    allpds[i].setSupportedOperators( SUPPORTED_OPS_PROPERTY );
  }
  else {
    allpds[i].setIsQueryable( false );
    allpds[i].setSupportedOperators( new String[0] );
  }

  allpds[i].setIsSelectable( fields[i].isReturnable() );

  // In the OmniFind connector, we'll distinguish special properties
  // (those obtained directly with an API method) with isSystemProperty.
  allpds[i].setIsSystemProperty( false );
  allpds[i].setLabel( fields[i].getID() );
}

Like system properties, global property descriptions are stored in the global item class by the connector. When the connector finds an OmniFind Field on a Result that matches a property description stored in the global item class, it uses that property description to create an II CE Property object for the OmniFind Field.

Properties with no property descriptions

Finally, the third kind of property in the example connector, the ad-hoc property, gives no way of determining what the property description should be. OmniFind does store this information somewhere internally, but does not expose it with the Search and Index API (SIAPI). All that you can do in this case is to define a generic, unrestrictive property description for each ad-hoc property, and use it to create each Property object.

Because the example connector cannot know about the existence of the ad-hoc properties ahead of time, it does not store their property descriptions in the global item class. Instead, it just creates the property description when needed.



Back to top


Example connector: Content retrieval summary

Summarily, you went through the following steps to create the Content object that is now returned using getRepoItem:

  1. Executed an OmniFind query for the item and got a Result
  2. Created a Content object to return
  3. Set all of the item attributes on Content, including item class name
  4. Set all of the system properties on Content (getters on the Result)
  5. Set all of the global properties on Content (matching Result::getFields() with Searchable::getAvailableFields())
  6. Set all of the ad-hoc properties on Content (the rest of Result::getFields())
  7. Returned the Content object to the client


Back to top


Folder retrieval

Folder retrieval follows a similar process because Folder is also a type of RepoItem in II CE. First, a Folder object must be created, and then a number of attributes must be set on the folder, including the item class (if folder item classes are supported). Properties are then set, and the Folder is returned to the requester.

In the example connector, II CE folders equate to Category objects from OmniFind, but not much is available on Category in the way of metadata. Consequently, the example connector doesn't really set any properties for folders, and the item class on the Folder will remain unset. The default item class on a RepoItem is null, so the item class on all Folder objects in the example connector will also be null.

There are a few notable ways in which folder retrieval differs from content item retrieval. The first is that they often will not go through the same channels. In OmniFind's case, the TaxonomyBrowser is used to retrieve categories, instead of a standard query. Second, Folder objects typically recognize a certain ID to be the root folder. Most commonly, this ID is "-1," which should be accounted for in your folder retrieval code. Finally, Folder objects have a getPaths() method that needs to be populated with all of the paths to the folder that are known by the repository. Paths can be added with Folder::addPath(String), where paths follow the form:

/rootfolder/folder1/.../folderN/targetfolder

In some cases, you may have to build this path yourself, but you should take care not to impact connector performance while doing so.

Although it's easy to miss, many connectors should also implement the getFoldersFiledIn(RepoItem) method, which will return all of the folders that a particular item is filed in. Notice that RepoItem is the method's parameter, which means that this method should function for all types of repository items if possible (much like getRepoItem(RepoItem)).



Back to top


Browsing in your connector

Most connectors will need to facilitate a quick property retrieval mechanism by executing ItemFinders appropriately. Quick property retrieval on a set of items is used in many II CE client applications, such as those that provide a folder browsing metaphor for navigation.

The executeFinder(ItemFinder finder) method in BaseBridge must be implemented to support ItemFinder execution in your connector. The last article discussed browsing metaphors at some length, but I will reiterate here that it's very desirable to provide a browse metaphor if it's at all possible. Graphical browsing, if it can be supported, will make repository interaction much more intuitive for your end users, and in general provides a better end user experience.


Figure 3. Browsing the OmniFind connector in the II CE Web Client
Browsing the OmniFind connector

ItemFinders facilitate browsing because they can retrieve all of the items contained within a Folder or WorkQueue, as well as any additional item handles that have been requested. This gives the client application two obvious possible user interfaces that could be implemented for browsing. The first is a simple, direct user interface to the repository, in which clicking on a certain repository folder will lead the user into that folder, much like a file system explorer would.

RepoBrowser
II CE includes a sample program that provides simple, direct browsing in IICE_HOME/docs/examples/java/swing/RepoBrowser.java.

The second is a Virtual Repository user interface, in which the client program can construct Virtual Folders that can contain any number of shortcuts to repository items, repository folders, and further Virtual Folders. This would give the user the power to define their own folder structure that spans multiple repositories. II CE includes a Virtual Repository API that supports virtual folders, shortcuts, and other concepts that make it easy to define your own folder hierarchies. The folder hierarchies that you define with the Virtual Repository API can then reuse parts of the folder hierarchies defined in one or more repositories.

Virtual Repository API
II CE includes a Web-based interface that leverages the Virtual Repository API, called the Web Client. For more on the Web Client, please consult the II CE documentation.

Executing ItemFinders

Your connector's role in ItemFinder execution is to execute ItemFinders not just for folders, but also for a list of arbitrary repository items because a virtual folder or other stored list could contain any combination of disconnected items in your repository. ItemFinder execution is fairly simple. You call ItemFinder::getSearchContainer and ItemFinder::getSelectionCriteriaHandles to determine what the user would like your connector to retrieve from the repository, and then you retrieve the specified properties of those items accordingly.

Your executeFinder(ItemFinder) returns a QueryResults object, which will hold a number of ResultRow objects. Each result row will hold a RepoItemHandle to identify the matching item and a number of selection properties. Selection properties are item properties that the user explicitly requests to be returned with each result row. This makes it possible for a user to browse through the repository while viewing certain properties on each item. Imagine a file system explorer on your computer -- as you browse through the various folders and directories, the explorer will show you things about each file, such as the name, creation date and the owner. Selection properties make this possible in II CE, with any item properties that the user chooses.

Example connector: ItemFinder

The OmniFind example connector provides a browse metaphor with the TaxonomyBrowser that the SIAPI makes available. The TaxonomyBrowser provides a hierarchy for categories; each category has access to its parent category, as well as its child categories. The OmniFind executeFinder method is then able to navigate folders as categories with the TaxonomyBrowser, and it queries for content in those folders. Say, for example, that an II CE user requests everything in the "MyContent" folder on the OmniFind connector. An ItemFinder would be sent to the OmniFind connector's executeFinder method, which would then do two things:

  1. Query OmniFind for results that are filed in "MyContent," and then build a ResultRow for each result
  2. Use the TaxonomyBrowser to get all of the child categories for "MyContent," and then build a ResultRow for each result

If a user requested specific RepoItemHandles in addition to the "MyContent" folder (the search container in this case), then the executeFinder method would execute queries for those as well and append their ResultRows to the QueryResults object returned by executeFinder.


Listing 10. Excerpt from executeFinder that operates on the search container:
				
// If we have a search container requested in the finder, then we will
// get every item that is contained in this search container and add
// it to the query results.
if( searchContainer != null ) {

  // Because "folders" are actually categories in OmniFind, we query to get
  // the content in a particular folder
  String queryString = _config.getPropertyString( OmniFindConfigBeanInfo.TAXONOMY )
                       + "::" + searchContainer.getInfo().getID();

  Result[] content = null;

  // There is a configurable connector property (specific to the OmniFind connector
  // that will control the maximum number of search results for a particular
  // category/folder.
  // If our finder maximum is greater than our "result per category" maximum,
  // then we will cap the limit at the configured value.
  int maxCategoryResults = _config.getPropertyInt(
  OmniFindConfigBeanInfo.MAX_CATEGORY_RESULTS );

  try {
    if( finder.getMaxResults() == 0 || finder.getMaxResults() > maxCategoryResults ) {
      content = ofsearch.executeOEQuery( queryString, maxCategoryResults );
    }
    else {
      content = ofsearch.executeOEQuery( queryString, finder.getMaxResults() );
    }
  } catch( Exception e ) {
    throw new InvalidFinderException( "An error occurred while processing the item 
    finder", e );
  }

  // Add all of the content items to the result set
  if( content != null ) {
    for( int c = 0; c < content.length; c++, resultCount++ ) {
      addContentResultRow( results, content[c], finder );
    }
  }

  // Now, we get the subfolders of this container.
  // "Subfolders" are actually child categories in OmniFind
  CategoryInfo[] subfolders = searchContainer.getChildren();

  // Add each subfolder to the result set
  if( subfolders != null ) {
    for( int sub = 0; sub < subfolders.length; sub++, resultCount++ ) {
      if( resultCount >= finder.getMaxResults() && finder.getMaxResults() != 0 ) {
        return results;
      }

      addFolderResultRow( results, subfolders[sub], finder );
    }
  }
}

Notice here that not every item added to the result set is necessarily going to have all of the selection properties available. A good example is that of the folder, which does not have properties in the OmniFind connector. Folders will never be able to return selection properties in the example connector; instead, they return null. Whenever you are adding an item to a result set, and that item does not contain one of the requested selection properties, null should be returned instead. null is used to indicate that the property does not exist. This is different from using the empty string, which indicates that the property exists but has no value.

ItemFinder exceptions and maximum results

There are a couple of other things that you will want to remember when you are implementing executeFinder for your connector. First, it's possible that during the retrieval of a particular result, you might encounter an exception. It could be for any reason -- a network issue, possibly a problem retrieving particular item from the native repository, or anything at all. In this case, you will add an exception to the QueryResults object, instead of adding a result. It's important that you catch this exception and add it to the QueryResults, and that you do not stop the execution of the ItemFinder. It's undesirable for an entire finder to fail because of one item.

A second thing to remember is that ItemFinders support a maximum number of returned results. This means that your executeFinder code will need to keep track of how many items it has added to the result set and cut off execution if it reaches the maximum number. An important detail is that exceptions count toward the maximum result limit, so make sure you count those as well.



Back to top


Querying in your connector

Now that we've gone over basic item retrieval and connector browsing, it's time to start talking about query behaviors. II CE supports two basic kinds of queries: property and full-text. It's possible to combine both queries into one query request, if your connector supports this behavior. Both types use the same query language, which is specific to II CE, but is an SQL-like query language.

At first glance, executing a query feels a lot like executing an ItemFinder. Your connector will implement an executeQuery(Query) method that will return a QueryResults object composed of ResultRows. Like the ItemFinder's ResultRows, the query result rows will each consist of a RepoItemHandle and the selection properties for this query. Queries, like ItemFinders, support limiting the number of results to a maximum.

The differences between Query and ItemFinder begin with the order in which results are returned to the connector. Query results need to be sorted according to the property specified in Query::getOrderByProperty(). If the order property is not numeric, then results should be sorted lexically. Don't sort in your Java code, however. Either II CE or the native repository should handle the sorting.

II CE will automatically sort your results by default, but it's desirable to use your repository's native sorting functionality, if possible, as this typically provides a faster sort. Repository sorting should be done as a part of the query; you should not send any results back to the repository for sorting. If your repository will be used to sort the results, then you should call QueryResults::setUseNativeSort(boolean) with a true argument, so II CE will not attempt to sort results on its own.

Query, unlike ItemFinder, also requires some relatively simple parsing to interpret query expressions correctly. Full-text query expressions are retrieved with Query::getFullTextQueryExpression(), and property query expressions use Query::getQueryExpression(). If both types of query exist on your query object, then your connector should execute a combined query. Combined queries should only apply if your connector can execute the combined query as a single operation. Your connector should not attempt to execute both types of query and then programmatically combine the result sets. In that case, it would be better to not support combined queries and to document that in the repository profile accordingly.

Finally, Query also includes the option to constrain a search to a particular container (folder or work queue). When this is the case, your query should only return results within that container or within that container's sub-containers recursively, depending on your repository's search behavior.


Figure 4. Combined query: Full-text is "word='FileSystemBridge'" and property is "Source='WinFS'"
Combined query: Full-text is word='FileSystemBridge'   and property is   Source='WinFS'


Back to top


Query expressions

Both of the methods that return query expressions will return a QueryExpression object to you. A QueryExpression is representative of the entire query, for example:


Listing 11. Example QueryExpression
				
author='Ryan Graciano' AND category='IICE' AND NOT rating<'3'

Each QueryExpression is composed of a number of QueryExpressionElement objects. A QueryExpressionElement can be one of three things:

  1. An expression of the form [property][operator][value]; for example: property='value'. Valid operators are: '=,' '<>,' '>,' '>=,' '<,' '<=,' 'LIKE,' 'ISNULL,' 'ISNOTNULL'
  2. A conjunction. Valid conjunctions are: 'AND,' 'AND NOT,' 'OR,' 'OR NOT'
  3. Parentheses '(' , ')'

Your connector, then, will need to iterate through the parsed QueryExpressionElements for each QueryExpression and use them to create a corresponding query statement for the repository.

Interpreting QueryExpressionElements

The algorithm you use to create a repository query from the query expression elements will vary depending on your connector's requirements, but there are a few things that you will always need to keep in mind. First, full-text queries and property queries are both composed of QueryExpressionElements, and the difference between the two involves the property='value' type of expression. In a property query, the property portion of the expression has an obvious, literal meaning. On a full-text query, the property portion of the expression is limited to one of two things: word or phrase. Each full-text query term needs to be interpreted correctly by your query code.

You should also notice that each value term is surrounded by single quotes, exemplified by this full-text query: word='hello'. You will need to strip these single quotes accordingly in your connector if your repository does not require them. II CE provides a static QueryExpression, unquote(String) convenience method for this task.

Double negatives

As you iterate over the query expressions, your algorithm may need to correct for double negatives. Consider a query expression like this one:

foo='bar' AND NOT abc<>'def'

Semantically, that would evaluate to:

foo='bar' AND abc='def'

If your repository does not support these types of doubles negatives, then this can affect how you rephrase this query into your repository's terms. This can be particularly problematic if you are simply iterating through QueryExpressionElements and converting each II CE query term into a native repository query term, and there is not a more extensive transformation of the query request.

Invalid literals

Invalid literals are another complication. Some repositories will use specific characters or strings to have special meanings in their query languages, which can cause problems when a user queries for content or property values containing those strings. OmniFind, for example, will search documents for links when it's given the string "link:" in the search string. When an II CE user executes a full-text search on the OmniFind connector for the word "link:," OmniFind interprets that as the link command, instead of the user's intended literal value.

In most cases, repositories will give an escape character or sequence for these types of special values, and if a value can be escaped by the connector, then it should be. The connector should replace all special values with their escaped literal values. This then makes them valid, and users will be able to search on them without ever knowing that they are problematic. However, if there is simply no way to execute a search on that special value, then it becomes an invalid literal. As such, you must declare it in the repository profile, which is discussed later in this article. Whenever your connector encounters an invalid literal in a search, it should throw an exception informing the user that this particular value cannot be searched on.

It's tempting to simply pass invalid literals forward to the repository, assuming that the user wanted to use the repository's special behavior for that literal, but you should not do this. You should never execute a search with an invalid literal, as this breaks the intended abstraction provided by II CE and the repository. Executing searches with invalid literals can create unexpected results for unknowing users. Further, it can encourage an undesired dependency by the users on the repository's special behavior.

Wildcards

Finally, your connector needs to properly handle wildcards. Wildcards, represented by '*' (multiple characters) and '?' (a single character) in the II CE query language, can only be used with the LIKE operator in either a full-text or a property search. If the repository supports wildcards, then your connector should properly translate '*' and '?' to the repository's native wildcard. In the case that wildcards are not supported by your repository, you will throw an exception when you encounter a wildcard with a LIKE operator. When wildcard characters are used with other operators, they should be treated as literals and simply be passed through to the repository, and no exception should be thrown.

As you do this, keep in mind that many repositories will still interpret the '*' and '?' characters with operators other than 'LIKE,' as they are common wildcards. If this is the case in your repository, then you will have to escape each character before passing it on. In some cases, you may need to declare '*' and '?' as invalid literals because your repository will not allow you to escape them. When that behavior is seen, your connector will throw an exception any time '*' and '?' are used as literals instead of wildcards.

Example connector: Interpreting queries

In addition to executeQuery(Query), the OmniFind example connector uses three methods to interpret queries: parseQuery(QueryExpression, boolean isFullTextQuery) iterates through the QueryExpressionElements in QueryExpression, translating II CE query terms to OmniFind SIAPI query terms. When a query expression is encountered (prop='value,' for example), it's sent to either parseFullTextExpression or parsePropertyExpression. When parseQuery encounters a conjunction, it saves it, so that the expression parsing functions can reference it and properly correct for negative conjunctions like AND_NOT and OR_NOT.


Listing 12. Abridged excerpt from parseQuery(QueryExpression)
				
for( int i = 0; i < queryexpression.getExpressionElementCount(); i++ ) {
  QueryExpressionElement element = qe.getExpressionElement( i );

  switch( element.getType() ) {

    case QueryExpressionElement.AND_NOT:
      lastConjunction = QueryExpressionElement.AND_NOT;
      queryString.append( ' ' );
      break;

    case QueryExpressionElement.EXPRESSION:
      if( isFullTextQuery ) {
        // Adds the full text expression to the query string
        parseFullTextExpression( element, queryString, lastConjunction, haveOr );
      }
      else {
        // Adds the property expression to the query string
        parsePropertyExpression( element, queryString, lastConjunction, haveOr );
      }
      break;

    // Abridged to only show relevant code

The parseFullTextExpression and parsePropertyExpression methods are then responsible for correctly translating the expression elements when they are found.



Back to top


Item creation

Item creation in your connector begins with implementing public String createItem(IContent newItem, String containingFolderID). Notice that this method signature is a little more restrictive than getRepoItem(RepoItemHandle), which allows retrieval of any item in the repository, including WorkItem or WorkQueue. createItem requires the IContent interface, which effectively limits content creation by this method to either Content or Folder. It's possible to create WorkItem and WorkQueue with another method, but that's outside the scope of this article. A future article may discuss workflow connector development in more detail.

Item creation starts when your connector determines what kind of item should be created:


Listing 13. Casting IContent
				
public String createItem(IContent newItem, String containingFolderID)
   throws NotLoggedOnException, ItemCreationException, UnsupportedActionException {

  validateLogon();

  if( containingFolderID != null ) {
    throw new UnsupportedActionException( "Creating content within a specific
    container is not supported by this connector" );
  }

  if( ! ( newItem instanceof Content ) ) {
    throw new UnsupportedActionException( "Only Content creation is supported by this
    connector" );
  }

  Content content = (Content) newItem;

  // Abridged to only show relevant code

Notice that an exception is thrown when a containing folder is specified but the connector does not support one. You should take care to throw exceptions in these situations, so the user will know that the action could not be performed exactly as they requested.

Example connector: Item creation

Once you get past parameter verification and you have cast the object to Content correctly, you must create the actual item in the repository. The OmniFind example connector begins by getting all of the properties from the Content object:


Listing 14. Looping through properties on Content
				
for( int i = 0; i < newItem.getPropertyCount(); i++ ) {
  Property prop = newItem.getProperty( i );

  String name = (prop.getName() != null ? prop.getName() : "" );
  String value = (prop.getValue() != null ? prop.getValue() : "" );

  // Abridged to only show relevant code

For each property that the example method gets, an OmniFind MetaField object is created and will be included when OmniFind's data listener is called with the DLDataPusher.pushData method. Your connector will probably use a similar loop, as it will run through all of the available properties and convert them to the proper native repository format.

Some repositories will require that you specify a mimetype during content creation, but the Content object will not tell you what this mimetype is. OmniFind is one such repository, and the example connector solves this problem with the com.venetica.vbr.util.MimeTypeUtil class, which is included in the II CE API but is not documented externally. This class will use a filename's extension to determine its mimetype:


Listing 15. Determining MIME Type for Contentobject
				
MimeTypeUtil util = new MimeTypeUtil();

String mimetype = util.getMimeType( content.getDefaultFileName() );

After metadata has been handled, native content has to be taken from the Content object and given to the repository. This is done with Content::getNativeContentUploadBuffer(), which returns a byte array. It's important that you do not confuse this method with Content::getNativeContent(), which also returns a byte array. The getNativeContent() method is a client method, and it's not intended for use in this context.

When the content has been created in the repository, your connector will need to return a String item ID for the item. Most commonly, the repository will have assigned an ID to the item and returned it to you on creation, and you will just return this ID to the user. II CE always assumes that the repository supplies the ID, but in OmniFind's Data Listener API, the user provides the item ID in the form of a URI. The OmniFind example connector will then actually create the item ID for OmniFind in the connector itself, using a randomly-generated ID and the item's name. This ID is then returned to the user as if the repository had created it, and it is the ID for subsequent retrievals.



Back to top


Supporting classes

In addition to ConnectorBridge.java, you will need to include several other files to build a connector EJB. Unlike ConnectorBridge, these classes and supporting files will require very little development time and planning, but they will be explained in the remainder of this article. For the most part, it's safe to use the example connector as a starting point for each supporting file, and I will point out what needs to be changed in your implementation.

Connector EJB Files

Java Classes

  • yourpackage.iice.yourconnector.YourConnectorBridge
  • yourpackage.iice.yourconnector.YourConnectorBridgeFactory
  • yourpackage.iice.yourconnector.config.YourConnectorConfigBeanInfo
  • yourpackage.iice.yourconnector.config.YourConnectorConfigFactory

Supporting Files

  • yourpackage/iice/yourconnector/config/yourconnector.properties
  • yourpackage/iice/yourconnector/ejb-jar.xml

Optional Files

  • yourpackage/iice/yourconnector/ibm-ejb-jar-bnd.xmi
  • yourpackage/iice/yourconnector/ibm-ejb-jar-ext.xmi
  • yourpackage/iice/yourconnector/*



Back to top


Package layout

What goes where?

You may be surprised to learn that there are no requirements as to what file goes where, and the above is simply a suggestion. So how does II CE know which class has the connector code, which class is the connector factory, and so on?

It all begins with the build script. The example build file has a line like this:


Listing 16. Defining the ConfigFactory in the MANIFEST
			
<replace file="${build}/TMP_MANIFEST.MF"
  token="@bridge_config_factory@"
  value="com.venetica.vbr.ejb.bridge.omnifind.config.OmniFindConfigFactory"
/>

This line is defining what ConnectorConfigFactory class (that's ConfigFactory, not BridgeFactory) will be placed in your EJB's META-INF/MANIFEST.MF file. II CE applications, such as the II CE Administration Tool, will use the manifest to figure out how to begin loading your connector.

II CE will load the ConnectorConfigFactory and use it to determine what ConnectorBridgeFactory class to instantiate. Inside the ConnectorConfigFactory, you'll see some code that looks like this:


Listing 17. Excerpts from ConnectorConfigFactory
				
public class OmniFindConfigFactory extends BridgeFactory {
  protected static String FACTORY_CLASS =
  "com.venetica.vbr.ejb.bridge.omnifind.OmniFindBridgeFactory";

  // ... Abridged to only show the relevant code ...

  public IContainable createObject(IContainer container, ConfigurationTree tree) {
    BridgeConfig bridge = new BridgeConfig( COMPONENT_ID, JNDI_NAME, FACTORY_CLASS,
props );
  }
}

As you can see, a new bridge configuration is being created for the BridgeFactory that you defined in ConnectorConfigFactory. If you configured your connector in the II CE Administration Tool, it would now be aware of that BridgeFactory.

From here, you're not far from actually creating a connector instance. Read the next section, about ConnectorBridgeFactory, to learn how the connector instance is created.



Back to top


Factory class

ConnectorBridgeFactory.java

II CE uses ConnectorBridgeFactory for two noteworthy functions. First, ConnectorBridgeFactory (which extends BridgeFactory) is used to create instances of your ConnectorBridge class, so you will need to implement the following method:


Listing 18. Creating a connector in the BridgeFactory
			
public IBridge createBridge( long bridgeInstanceID, String systemID,
BridgeConfig config )
  throws BridgeCreationException {

  _systemID = systemID;
  _description = config.getDescription();

  return new ConnectorBridge( getSystemID(), bridgeInstanceID, config );
}

Second, ConnectorBridgeFactory contains all of the information that is required to build a repository profile for your connector. The repository profile is used by the client to determine what your connector supports, which makes error handling and feature determination much easier for client applications. Take a look at the following hypothetical client code to see what I mean:


Listing 19. Client code
				
Repository repo = user.getRepositoryByID( "MyConnector" );
RepositoryProfile profile = repo.getRepositoryProfile();

if( profile.getCanCreateContent() ) {
  drawCreateContentButton();
}
else {
  addWarning( "Repository does not support content creation." );
}

It's very important to fill out the repository profile correctly so clients will have an accurate picture of what your connector supports. Profiles are not just limited to boolean values about supported features; they also include information, such as the maximum number of query results and the invalid literals for each kind of repository query.

Completing ConnectorBridgeFactory is very easy. Simply override each abstract method in BridgeFactory, and change it to return true or false, as appropriate. In some cases, you may need to return an array of String (such as when defining invalid literals), but these can be defined statically or right inside of the method.



Back to top


Configuration files

ConnectorConfigBeanInfo.java, ConnectorConfigFactory.java, connector.properties

ConnectorBridge will have access to a BridgeConfig, which will hold all of the configuration information that your connector requires. The II CE Administration Tool is used to set up everything that will eventually be held inside of the BridgeConfig. When the Administration Tool is loaded, it locates all of the connector EJB JAR files in the IICE_HOME/lib directory. For each JAR file, it uses the ConnectorConfigBeanInfo class to determine the properties required by the connector. When a user then configures an instance of that connector in the Administration Tool, they are given the option to set each property defined by ConnectorConfigBeanInfo.

ConnectorConfigBeanInfo is a very straightforward class. You should simply be able to copy and paste the code from OmniFindConfigBeanInfo into your own class and alter, add, or remove properties accordingly.

Like ConnectorConfigBeanInfo, the ConnectorConfigFactory class is relatively simple. ConnectorConfigFactory is used to determine how your connector should be created and accessed; for example, it holds the correct JNDI name and factory classes for your connector. OmniFindConfigFactory can also be cut and pasted into your own class, but you should be careful to change the instance variables to correctly reflect the package and class names that are correct for your connector. As you do this, note that the COMPONENT_ID instance variable should be left alone, as it's the same for all custom connectors.

Finally, the connector.properties file provides a default configuration for your connector. It's recommended that you provide a default value for every property, even if some properties must be configured by the user. Default values can be explanatory, as they will show the user the proper format for configuration value or provide some clues about the context in which the property is used by the connector. The connector.properties file will be automatically loaded from the same package as the ConnectorConfigFactory class, so remember to keep them together.


Figure 5. II CE Administration Tool with OmniFind connector
II CE Administration Tool with OmniFind connector


Back to top


Deployment descriptor

ejb-jar.xml

The EJB deployment descriptor, ejb-jar.xml, is used by the application server to set options on your connector EJB at deployment time. This file is necessary to deploy your EJB into a J2EE application server, and you should be able to copy/paste the ejb-jar.xml file that the OmniFind example connector uses into your own ejb-jar.xml. Take care to replace the OmniFind values with proper values for your connector. Be careful as you edit the ejb-jar.xml file; you should not replace any value that does not include the text "OmniFind" unless you are very experienced with deployment descriptors and II CE EJB deployment.



Back to top


Optional files

ibm-ejb-jar-bnd.xmi, ibm-ejb-jar-ext.xmi, and additional files

Your connector can also include a number of additional files. First, your connector code does not need to be restricted to the classes mentioned in this article. You are certainly free to include any number of other classes that your connector code requires, which you should package into the connector EJB accordingly.

You might also want your connector EJB to take advantage of various functions that are specific to certain application server versions. For example, the OmniFind example connector includes two files that are specific to IBM WebSphere Application Server: ibm-ejb-jar-bnd.xmi and ibm-ejb-jar-ext.xmi.

The -bnd.xmi file defines JNDI bindings for the EJB, so that they do not need to be specified at deployment time. This simplifies deployment for WebSphere Application Server users.

The -ext.xmi file defines WebSphere-specific extensions for the EJB. In the OmniFind connector's case, the ext.xmi file is setting a bean timeout for all of the OmniFind connector EJB instances in the application server. The bean timeout does not affect passivation; rather, it tells WebSphere Application Server when the bean should actually be destroyed after a certain period of time, specified in seconds.

It's not necessary that you include either of these WebSphere Application Server-specific files with your EJB, but if you intend to deploy on WebSphere Application Server, they are nice to have. They will also be ignored by other application servers, so there is no harm in including them.

Along the same lines, there is no harm in including extensions that are specific to other application servers. If you have your own deployment extension files that you would like to include with your EJB, then you should feel free to do so.



Back to top


Building and deploying your connector

An example Ant script included with this connector can be easily modified to build your connector. Using this script, build-omnifind.xml, is fairly simple.

  1. Make sure that you are running at least version 1.5 of Ant. If you don't already have Ant, it can be obtained from the Apache Ant project Web site. (See Resources.)
  2. Rename the build-omnifind.xml script to build-yourconnector.xml, and place it in the following directory, where IICE_HOME is the directory in which you have installed II CE: IICE_HOME/docs/examples/java.
  3. Copy your connector source code and all associated files to the same directory as in step 2. For example, if your connector is in the package net.yourcompany.myconnector, then you should have the following directory structure: IICE_HOME/docs/examples/java/net/yourcompany/myconnector
  4. Modify the build-yourconnector.xml file for your environment. Update JAVA_HOME, J2EE_HOME, and IICE_HOME as appropriate.
  5. Add the appropriate JAR files that your connector requires to the build classpath. There will typically be at least one library that you need to connect to your repository, so the build script will need it to build your connector JAR file. Here's an example from build-omnifind.xml:

    Listing 20. Classpath setup in your build script
    
    <!-- OmniFind JAR Directory -->
    <!-- Change this to the directory in which all of the required supporting JARs
    for your connector are located -->
    <property name="OE_LIBHOME"value="C:/Program Files/IBM/es/lib" />
    
    <!-- Project classpath -->
    <path id="classpath">
    
      <!-- Abbreviated for example; more JARs are included in the actual file -->
    
      <!-- OmniFind JARs. These JARs are required by any client to OmniFind that
      uses the same functionality we use -->
      <!-- Change these JARs to whatever JARs your connector requires.
      <fileset dir="${OE_LIBHOME}">
        <include name="esapi.jar"/>
        <include name="siapi.jar"/>
        <include name="es.dl.client.jar"/>
        <include name="es.oss.jar"/>
      </fileset>
    
    </path>
    

  6. Correct all of the package references. Every instance of com/venetica/vbr/ejb/bridge/omnifind in the build file, replace with the correct package for your connector.
  7. Correct the reference to the config factory, com.venetica.vbr.ejb.bridge.omnifind.config.OmniFindConfigFactory. That should be changed to the correct ConnectorConfigFactory class for your connector.
  8. Add any additional files that your connector requires, such as EJB deployment descriptor extensions, and add these to the build file like so:

    Listing 21. Including deployment descriptors in your EJB JAR
    
    <!-- Add extra files to your EJB JAR file here. For example, deployment
            descriptor extensions for specific application servers. -->
    
    <copy todir="${TEMPDIR}/META-INF"
          file="${src}/com/venetica/vbr/ejb/bridge/omnifind/ejb-jar.xml"
          overwrite="yes" />
    
    <copy todir="${TEMPDIR}/META-INF"
          file="${src}/com/venetica/vbr/ejb/bridge/omnifind/ibm-ejb-jar-bnd.xmi"
          overwrite="yes" />
    
    <copy todir="${TEMPDIR}/META-INF"
          file="${src}/com/venetica/vbr/ejb/bridge/omnifind/ibm-ejb-jar-ext.xmi"
          overwrite="yes" />
    

  9. Change the reference from omnifind_connector.jar to your_connector.jar.
  10. Run the build script for your connector: ant -f build-yourconnector.xml. When the build script completes, you should have a your_connector.jar file located in IICE_HOME/docs/examples/java/build.

After your connector JAR is built, copy it to the IICE_HOME/lib directory. Then run the IICE_HOME/bin/rebuild_ear.sh script on Unix, or IICE_HOME\bin\rebuild_ear.bat on Windows platforms. The VeniceBridge.ear file should be rebuilt, using everything in IICE_HOME to generate a new EAR file.

Alternatively, the connector can be inserted into VeniceBridge.ear manually:

  1. Add your_connector.jar to VeniceBridge.ear:
    cd $IICE_HOME
    copy lib/your_connector.jar.
    jar uf VeniceBridge.ear your_connector.jar
    

  2. Extract application.xml from VeniceBridge.ear:
    jar xf VeniceBridge.ear META-INF/application.xml

  3. Open application.xml in a text editor, and add the following lines inside of the <application> tag:
    <module>
      <ejb>your_connector.jar</ejb>
    </module>
    

  4. Update application.xml in the EAR file:
    jar uf VeniceBridge.ear META-INF/application.xml
    

When your VeniceBridge.ear file has been updated with your connector JAR, it should be redeployed to your application server. This process will depend on the application server that you are using, and the II CE documentation should be consulted with more details. When you redeploy, keep in mind that your application server will require access to the JAR files for your repository, just like your build script required access to those JARs. Each JAR file required by your connector to access the repository should be added to the application server's classpath.




Back to top


Download

DescriptionNameSizeDownload method
DeliverablesOmniFindConnector_src.zip36KBFTP|HTTP
Information about download methods


Resources

Learn

Get products and technologies

Discuss


About the author

Ryan Graciano is a member of the II Content Edition engineering team in Charlotte, North Carolina. His current focus is on providing test automation for WebSphere Information Integrator Content Edition.




Rate this page


Please take a moment to complete this form to help us better serve you.