 | 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.
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.
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
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.
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.
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?]" );
}
}
|
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.
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.
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:
- Getters on the
Result,
such as getTitle(), getScore(), and getLanguage()
- OmniFind
Fields (analogous to II CE properties)
that are available from getFields() on the Result object and exist globally in Searchable::getAvailableFields()
- 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
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.
Example connector: Content retrieval summary
Summarily, you went through the following steps
to create the Content object that is now returned using getRepoItem:
- Executed an OmniFind query for the item and got a
Result
- Created a
Content object to return
- Set all of the item attributes on
Content, including item class
name
- Set all of the system properties on
Content (getters on
the Result)
- Set all of the global properties on
Content (matching Result::getFields()
with Searchable::getAvailableFields())
- Set all of the ad-hoc properties on
Content (the rest of Result::getFields())
- Returned the
Content object to the client
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)).
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
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:
- Query OmniFind for results that are filed in
"MyContent," and then build a
ResultRow for each result
- 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.
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'"
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:
- An expression of the form
[property][operator][value]; for example:
property='value'. Valid operators are:
'=,' '<>,' '>,' '>=,' '<,' '<=,' 'LIKE,' 'ISNULL,' 'ISNOTNULL'
- A conjunction. Valid conjunctions are:
'AND,' 'AND NOT,' 'OR,' 'OR NOT'
- 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.
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.
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/*
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.
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.
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
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.
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.
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.
- 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.)
- 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.
- 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
- Modify the build-yourconnector.xml
file for your environment. Update JAVA_HOME, J2EE_HOME, and IICE_HOME as
appropriate.
- 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>
|
- 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.
- 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.
- 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" />
|
- Change the reference from omnifind_connector.jar to your_connector.jar.
- 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:
- Add your_connector.jar to VeniceBridge.ear:
cd $IICE_HOME
copy lib/your_connector.jar.
jar uf VeniceBridge.ear your_connector.jar
|
- Extract application.xml from VeniceBridge.ear:
jar xf VeniceBridge.ear META-INF/application.xml |
- Open application.xml in a text editor, and
add the following lines inside of the <application> tag:
<module>
<ejb>your_connector.jar</ejb>
</module>
|
- 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.
Download | Description | Name | Size | Download method |
|---|
| Deliverables | OmniFindConnector_src.zip | 36KB | FTP | HTTP |
|---|
Resources Learn
-
"Integrate unstructured content into a distributed federated system, Part 1" (developerWorks, December 2005): Get details on how to plan and build a connector for WebSphere Information Integrator Content Edition.
-
"A technical tour of IBM WebSphere Information Integrator Content Edition (developerWorks, February 2005): Take a "technical tour" of the robust, J2EE-based architecture and technology behind WebSphere II Content Edition, and find out how to integrate your enterprise applications with relevant content.
-
"WebSphere Information Integrator Content Edition: Planning, Configuration, and Monitoring Guide" (IBM, December 2005): This Redbook provides an overview of WebSphere II and documents the procedures for implementing WebSphere II CE in a heterogeneous AIX and Windows distributed environment for an insurance industry call center scenario.
-
IBM WebSphere Information Integrator Content Edition: Learn more about this product.
-
IBM WebSphere Information Integrator OmniFind Edition: Learn more about this product.
-
Enterprise JavaBeans Specification (java.sun.com)
-
EJB restrictions FAQ (java.sun.com): Guidelines, patterns, and code for end-to-end Java applications.
-
J2EE reference material (java.sun.com)
-
Java tutorial: Serialization (java.sun.com): Learn how to serialize objects and provide object serialization for your classes.
-
J2SE reference material (java.sun.com)
-
Stay current with
developerWorks
technical events and webcasts.
-
developerWorks Information Management zone: Learn more about DB2. Find technical documentation, how-to articles, education, downloads, product information, and more.
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 |