Interact with data repositories on behalf of client applications. The Information Service abstracts various information service sources including SAS Metadata Server, LDAP, and WEBDAV.

# Information Service

## Overview

The information service provides convenient access to data repositories. It will connect to multiple repositories and perform searches that span repositories.

## Terms

Entity | An unit of information from a persistent store.
Filter | A means of restricting search results to an interesting set of items
Repository | A persistent storage mechanism

## Model

Classes in this package include a filtering mechanism that will allow creating a single filter to search disparate repositories, but will allow repository-specific information so that efficient searching can be achieved.

## Interface

The Information Service provides methods for connecting to repositories by repository definition (a method exists to get these definitions as well) or by name. It also provides methods to search all repositories connected to by a user and aggregate the results, fetch specific objects, and create new metadata objects.

There are also methods for searching multiple repositories. All repositories a user is connected to can be searched, or only repositories using a specific protocol (such as "dav"). A single filter, or a filter array can be provided to limit the result set being returned.

## Metadata Access Classes ("Smart Objects")

Search results that are returned from the repositories are wrapped by classes that implment the `com.sas.services.information.MetadataInterface` interface. This provides a common means for accessing basic information about the object so it can be rendered and processed. Commonly used objects will have a specific subclass which implments a functional interface which should hide some of the data model specifics. It should also provide repository neutrality for classes that may be contained in more than one type of backing store. For instance, a stored process may be stored in SAS Metadata Server as a ClassifierMap, and in LDAP as a SasStoredProcess. A `com.sas.services.information.metadata.StoredProcess` class hides these differences and provides accessor methods for keywords, servers which can execute it, and path information. All of this without having to know the underlying model, or having to know which type of repository the data came from. The StoredProcess class also has a method for interacting with the Stored Process Foundation Service to create a new instance of a service object which can then be executed.

Every Metadata object provides an entity key that can be used to determine the type of the object and locate it in the repository at a later date, if desired. The entity key is composed of a repository-neutral type, if defined, a '+' character, a url that has the protocol, server, port, repository ID and the repository type. This key may be stored as a bookmark so the item can be referenced at a later date.
Example: Channel+omi://d5296.us.sas.com:4059/A5CK95WU.AB000009/ITChannel

## Filters

To make the cross-repository search as generic as possible, the filter mechanism is repository neutral. The client may set the type to search for, and create filter components that limit the result set to those items it is interested in.
Filter components are relationship tests to perform on repository entities to limit the results of a search. Components can have subcomponents to build complex search filters.
Filter components have two aggregation modes: ALL_OF and ANY_OF. These determine how subscomponents are used. If the aggregation mode is ALL_OF, all of the subcomponent tests must be true for the entity to pass. If ANY_OF is used, if any of the tests is true, the entity passes the filter.
There are four relationship checks: EQUALS, CONTAINS, STARTSWITH, and ENDSWITH. For a component with a given key and value setting, an entity will pass the test if it has an attribute that matches the key, and:
If the relationship is EQUALS, the attribute has a value that equals the value in the component.
If the relationship is CONTAINS, the attribute has a value that contains the value in the component.
If the relationship is STARTSWITH, the attribute has a value that starts with the value in the component.
If the relationship is ENDSWITH, the attribute has a value that ends with the value in the component.
The Filter class can generate search strings appropriate to the known repositories, or in a generic format that can be parsed and used to produce search filters for customer-specific repositories.

### Example - Creating a Filter

#### A Simple Filter

`Filter simple = new Filter( "subject", FilterComponent.CONTAINS, "Sales" ); simple.setType( "Channel" );`

This creates a simple filter to search for channel entries that have a subject that contains the word "Sales".

`Filter complex = new Filter(); FilterComponent keyword = new FilterComponent( "keyword", FilterComponent.EQUALS, "weekly" ); FilterComponent base = new FilterComponent( "Subject", FilterComponent.CONTAINS, "Sales" ); base.addComponent( keyword ); base.setAggregation( FilterComponent.ALL_OF ); complex.setFilterComponent( base ); complex.setType( "Channel" );`

This creates a filter that looks for entries of type "Channel" that have a subject that contains the string "Sales" and a keyword that equals "weekly".

This mechanism works the same regardless of the repository that's being searched. However, some searches can be made much more efficiently if certain repository-specific options are provided. So the filter allows those options to be specified if the search client knows what repository is being searched, and what options to provide to improve that search. For instance, the Filter allows the client to set the specific SAS Metadata Server repository to search, a template to use to retrieve data, and the flags to pass to the SAS Metadata Server to use when processing the request. For LDAP, a search base can be provided to limit the scope of the search.
It is also useful to create type-specific subclasses of the Filter class that can be used to search for given metadata types. Rather than building the filter components manually, you can set the desired attributes and the Filter object will build the appropriate filter components and generate the correct search string. For instance, there is a StoredProcessFilter class that allows a user to search for Stored Process objects by keyword or name. Just create a new StoredProcessFilter and set the name, or add keywords to search for and pass it to the search method.

## Repositories

A repository is a persistent storage mechanism for metadata and/or content. It is typically a server that implements a protocol for providing access to its contents (e.g., SAS Metadata Server, LDAP, or WebDAV). The Information Service provides methods for connecting to a repository, searching for repository definitions, and searching the connected repositories for information.

Most repositories, besides providing access to stored information, also implement some sort of authentication mechanism. The typical installation will, presumably, use one of these repositories as the primary authentication mechanism. The User Service, Authentication Service and the Information Service work together during the authentication process so that after a successful authentication, the User Context is created and has a handle to the repository(s) that the user authenticated with. From this starting point, the Information Service can be used to look up other repositories that the user may have access to using the `findServers` method, which locates definitions for servers that implement one of the known protocols (from the repository configuration). The result of this call is a set of repository definitions that can then be passed into the `connect` method to try to establish a connection to the server.

The primary use for the Information Service is to provide a federated search mechanism for any repositories a user has a connection to. The method takes as input, the UserContext for the user who is doing the search, and a filter. The connected repository handles are searched using the filter and the results are collected and returned. All of the results will implement the `MetdataInterface`. Since a search can return multiple types from different repositories, it's up to the application to determine what specific type of entry each item is and process it accordingly. Through the MetadataInterface, the application has access to the object type, name and description.

The Information Service also provides a convenience method for fetching an item using the entity key. It gets the url from the key, and looks through the connected repository handles to see if there's a connection to the server that contains the item. If so, it uses the repository to fetch the item and return the resulting `MetdataInterface` object for it. Currently, the Information Service will not try to establish a connection to a server in this method if one does not already exist, but that function may be added at a future date.

The repositories also have methods for adding items to the store, and deleting items. Since these functions necessarily only apply to a single repository instance, they aren't implmented in the Information Service. The update function is handled through the `update` method of the object being updated.

## Examples

login.config
    /** Login Configuration for the Foundation Services Sample Application **/
    PFS {
       com.sas.services.security.LdapLoginModule     optional
                           "host"="d5296.us.sas.com" "port"="389"
                           "privilegedUser"="cn=sasidb,o=SAS Institute,c=US"
                           "privilegedPassword"="sasidb1"
                           "base"="o=SAS Institute,c=US"
                           "groupbase"="ou=Groups,o=SAS Institute,c=US"
                           "loginbase"="o=SAS Institute,c=US"
                           "domain"="ldap";

       com.sas.services.security.OMILoginModule     optional
                           "host"="d5296.us.sas.com" "port"="4059"
                           "repository"="A0000001.A5CK95WU"
                           "domain"="CARYNT";
    };

This is a JAAS formatted configuration file. It defines the login modules to use for an authentication operation, and initializes them with the properties provided. The default server node, port, and other information useful to completing the authentication process.

                    UserContextInterface userMe = _userService.newUser( "sasgwi",
                                                                        "sasgwi1",
                                                                        "ldap" );

This creates a new user context, and attempts to authenticate the user with the give user ID, password, and authentication domain. Since the domain that's given ("ldap"), is compared with the domain in the login.config for each module, and if different, will skip that module.

If the authentication with either or both modules is successful, a handle to the repository that was connected to will be saved in the UserContext. Also, any credentials that are in the repository that are available to the user will be retrieved and saved in the UserContext.

    /*
    * Look through the connected repositories for other
    * repositories.  Their definitions will be returned in the
    * iterator.  Go through the iterator and try to connect to
    * any servers that were found.
    */
    Iterator serverIter = _informationService.findServers( userMe ).iterator();
    while( serverIter.hasNext() )
    {
        RepositoryDef reposDef = (RepositoryDef)serverIter.next();
        if ( ! (_informationService.connect( userMe, reposDef ) ) )
        {
            System.out.println( "connection to: " +
                reposDef.getProtocol() +
                "://" + reposDef.getHost() + ":" +
                reposDef.getPort() + " failed" );
        }
        else
        {
            System.out.println( "connection to: " +
                                reposDef.getProtocol() +
                                "://" + reposDef.getHost() + ":" +
                                reposDef.getPort() +
                                " succeeded" );
        }
    }

This code fragment uses the information service to find any server definitions in the repository(s) that the user already has connections to. If any are found, the application tries to establish a connection to them.

    /*
     * Create a simple filter and perform a search.
     */
    Filter filter = new ChannelFilter();
    filter.setLdapBase( "cn=SAS,o=SAS Institute,c=US" );
    it = _informationService.search( userMe, filter ).iterator();
    while( it.hasNext() )
    {
        Object o = it.next();
        if ( o instanceof com.sas.services.information.metadata.ChannelInterface )
        {
            com.sas.services.information.metadata.ChannelInterface channel =
                (com.sas.services.information.metadata.ChannelInterface) o;
            if ( channel.getName().equals("garytest") )
            {
                channel.setDescription( "Test for Gary" );
                channel.update();
            }
            System.out.println( "Found " + channel.getEntityKey() + "." +
                channel.getTrackingId() );
        }
    }
    Results
    Found Channel+ldap://d5296.us.sas.com:389/saschannelcn=Product Development,
        cn=saschannels,sascomponent=sasPublishSubscribe,cn=SAS,o=SAS Institute,c=US/saschannel.5
    Found Channel+ldap://d5296.us.sas.com:389/saschannelcn=Quality Assurance,
        cn=saschannels,sascomponent=sasPublishSubscribe,cn=SAS,o=SAS Institute,c=US/saschannel.6
    Found Channel+ldap://d5296.us.sas.com:389/saschannelcn=Product Announcements,
        cn=saschannels,sascomponent=sasPublishSubscribe,cn=SAS,o=SAS Institute,c=US/saschannel.7
    Found Channel+ldap://d5296.us.sas.com:389/saschannelcn=Human Resources,
        cn=saschannels,sascomponent=sasPublishSubscribe,cn=SAS,o=SAS Institute,c=US/saschannel.8
    Found Channel+omi://d5296.us.sas.com:4059/A5CK95WU.AB000001/ITChannel.18
    Found Channel+omi://d5296.us.sas.com:4059/A5CK95WU.AB000002/ITChannel.19
    Found Channel+omi://d5296.us.sas.com:4059/A5CK95WU.AB000003/ITChannel.20
    Found Channel+omi://d5296.us.sas.com:4059/A5CK95WU.AB000004/ITChannel.21

This example creates a ChannelFilter and performs a search of the users connected repositories. Using a ChannelFilter will limit the results to one type: Channels. Using a generic Filter with no type set would have resulted in a mixed result set. Channels were found in multiple repositories, as the entity keys reflect.

    MetadataInterface mi = _informationService.fetchEntityByKey( user,
        "Channel+ldap://d5296.us.sas.com:389/saschannelcn=Human Resources," +
        "cn=saschannels,sascomponent=sasPublishSubscribe,cn=SAS,o=SAS Institute," +
        "c=US/saschannel" );
    if ( mi != null )
    {
        System.out.println( "Got channel " + mi.getName() );
    }

This call retrieves a data item based on an entity key. This key would normally be stored in a user profile, or some other location as a bookmark for later access, rather than hard coded.

# Extending Information Services

While the Information Service is useful as it is, its real power comes from its extensibility. There are a basic set of classes and services that may be used to access information and manipulate it, but it may not provide the most useful functional interface for all types of data.

## Creating New Metadata types

Metadata objects or "smart objects" are wrapper objects that hide the underlying data model, and specifics of different repository types. They are intended to provide data access and update capabilities. They are not intended to provide mechanisms to perform functions against physical resources that the metadata may represent. For instance, the StoredProcess smart object provides access to the stored process metadata. It does not, however, provide methods to execute the stored process and get results. The smart object can be used to instantiate an object that has these capabilites, though.

#### The Interface

The first thing to consider when creating a new smart object is that you want to create an interface that's functionally useful, and not necessarily directly reflective of the underlying model. Provide methods that access data that's useful to a client, even if it involves getting data from other objects.

The first task is to create an interface for your new type that extends `com.sas.services.information.metadata.MetadataInterface`. This is the base class that all the Information Service repositories work with. As a SAS standard, the interface name should end with "Interface", so if you're creating a "Widget" smart object, your interface would be "WidgetInterface". Since MetadataInterface is a remote interface, all public methods have to throw java.rmi.RemoteException. To be consistent with MetadataInterface, they should also throw com.sas.services.ServiceException.

#### The Implementation

Once the interface is done, you can start on your implementation. All smart objects inherit from the `com.sas.services.information.metadata.Metadata` class. The implementation must deal with the underlying, repository-specific objects. For objects in OMR, that means jOMA classes. Other repository types have sets of classes for representing the data that comes from them. Hiding these details is what the smart objects are for. There are two models that you can use for your implementation. If your objects are only contained in one repository type, like OMR, you probably only need one implementation. If your objects can come from more than one type of repository, like OMR and LDAP, you can have one implementation that can work with both types of repositories, or you can have different implementations. One of the advantages of having different implementations is that they can have different inheritance. Since any object in LDAP can have children, it can be useful for LDAP implementations to inherit from Folder, while the OMR implementation inherits from Metadata (since only a Tree can have children in OMR). The preferred implementation is to have one per repository type, for example, an OMRWidget that implements the WidgetInterface for the OMR repository, and an LDAPWidget for the LDAP repository.

Your first order of business, of course, is constructors. For OMR objects, you'll need five constructors. One is the default constructor that takes no arguments. Then one each that takes as an argument the jOMA object's interface, implementation, and stub. So, if the Widget is based on a Transformation object, you'd have constructors of:

        public OMRWidget() throws RemoteException
        {
            super(OMRWidget.class);
        }

        public OMRWidget(com.sas.metadata.remote.Transformation o) throws RemoteException
        {
            super(o, OMRWidget.class);
        }

        public OMRWidget(com.sas.metadata.remote.impl.TransformationImpl o) throws RemoteException
        {
            super(o, OMRWidget.class);
        }

        public OMRWidget(com.sas.metadata.remote.impl.Transformation_Stub o) throws RemoteException
        {
            super(o, OMRWidget.class);
        }

You'll notice that each constructor calls a super constructor that takes the class as an argument. That's because the class is used to determine RMI socket factories. If you're going to allow your class to be extended, you'll also need to add those constructors as protected methods.

        protected OMRWidget(Class c) throws RemoteException
        {
            super(c);
        }

        protected OMRWidget(com.sas.metadata.remote.Transformation o, Class c)
            throws RemoteException
        {
            super(o, c);
        }

        protected OMRWidget(com.sas.metadata.remote.impl.TransformationImpl o, Class c)
            throws RemoteException
        {
            super(o, c);
        }

        public OMRWidget(com.sas.metadata.remote.impl.Transformation_Stub o, Class c)
            throws RemoteException
        {
            super(o, c);
        }

You'll need one more constructor that creates a new object that takes the Repsitory, a name, and a parent folder as arguments:

        public OMRWidget( RepositoryInterface repos, String name, FolderInterface parent )
            throws RemoteException
        {
            super(OMRWidget.class);

            if ( (repos == null) ||
                 (name == null)) throw new IllegalArgumentException();

            try
            {
                OMIRepositoryInterface omirepository = (OMIRepositoryInterface)repos;
                Transformation transform = (Transformation)
                    omirepository.createMetadata(name, MetadataObjects.TRANSFORMATION);

                // Why this is important will be clear later.
                transform.setTransformRole = "Widget".
                _repositoryObject = transform;
                _repositoryFlag = Metadata.REPOSITORY_OMR;
                setRepository( repos );
                if ( parent != null )
                {
                    try
                    {
                        parent.addItem( this );
                    }
                    catch( Exception ex )
                    {}
                }
            }
            catch(Throwable t)
            {
                logMessage(LoggerInterface.WARN, RB.getStringResource(
                "Metadata.accessexcept.msg.txt"),
                "com.sas.services.information.metadata.OMRDataTable",
                t);
            }
        }

Especially note the construct:

        try
        {
        }
        catch(Exception ex)
        {
            logMessage(LoggerInterface.WARN,...);
        }

This is the standard mechanism for logging exceptions from the metadata classes. If the method signature throws ServiceException, rather than logging the message, you should;

        try
        {
        }
        catch(Exception ex)
        {
            throw new ServiceException(ex, ex.getMessage());
        }

There are some details you need to keep in mind when implmenting a Metadata subclass. There will most likely be instances where the data you will be returning will be another instance of a Metadata subclass that you created. For instance, the com.sas.services.information.metadata.ChannelInterface will return a DirectoryInterface object for the getDefaultArchivePath method. The correct way to create this is by using the com.sas.services.information.Factory object:

        MetadataInterface mi = _repository.factoryProcess( object );

Where object is the object that came from the repository. Older smart object classes created their own factory, but using the repository's factory allows it to better control the smart object cache and do any necessary processing of the smart object (like calling setRepository).

The Metadata class has several methods for handling logging:

        public void logMessage( int level, String message, String context,
                                Throwable t) throws RemoteException
        public void logThrowable( int level, String message, String context,
                                  Throwable t) throws RemoteException
        public void logFormat(int level, String context, ResourceBundle bundle,
                              String key, Object arg0) throws RemoteException
        public void logFormat(int level, String context, ResourceBundle bundle,
                              String key, Object arg0, Object arg1)
            throws RemoteException
        public void logFormat(int level, String context, ResourceBundle bundle,
                              String key, Object arg0, Object arg1, Object arg2)
            throws RemoteException
        public void logFormat(int level, String context, ResourceBundle bundle,
                              String key, Object arg0, Object arg1, Object arg2,
                              Object arg3) throws RemoteException
        public void logMessage(int level, String message) throws RemoteException

In each of these methods, level is one of the levels defined in com.sas.services.logging.LoggerInterface (DEBUG, INFO, WARN, ERROR, FATAL). The context should be the class name of the calling class. In general, logging above the DEBUG level should be kept to a minimum. If an error condition occurs, an exception (preferably a ServiceException) should be thrown.

#### The Configuration

Now that you have an interface and an implementation that represents the data, you need to have that class instantiated by the repositories when that type is returned by a search or a fetch. To accomplish this, you have to update the Factory section of the Information Service configuration. The configuration will include the protocol(s) of the repositories that your type might be in (omi, ldap, dav), the repository-specific type (e.g., Transformation), an optional filter so that your type can be distinguished from others of the same type, but possibly different semantics, and the full-qualified Java class name to instantiate.

In order to update the configuration, use the Foundation Services Manager plug-in to the SMC. Open the "Foundation Service Manager", then the deployment and group you wish to update. Select "Information Service" (the exact name may be different, but it should include 'Information' in it somewhere, then right-click on it to bring up the options menu. Select "Properties". This will bring up the Information Services configuration page. Select the "Service Configuration" tab, then press the "Edit Configuration" button. Select the "Factories" tab, then select the appropriate protocol in the combo box (omi for objects in the metadata server). If your type can be contained in multilpe repository types, you will have to enter a configuration for each protocol. Check to see if the metadata type is already listed in the "Types" list. If so, select it and press the "Edit" button to add your new class to this type. Otherwise, press the "New" button to bring up the dialog to create a new type definition.

If this is a new entry, enter the metadata type (e.g., Transformation) in the Type field. Then click the "Add..." button to bring up the dialog to create a new factory action.

You'll want to create a "constructor" action type. For now, don't worry about the other two types. Enter the fully-qualified (including the package) name of your new Metadata subclass. If the type you're using is a common, generic type (like Transformation or DeployedComponent), you'll probably need to enter a filter. The filter has the format @attribute='value'. Where attribute is the name of an attribute of the metadata object. In our example, a Tranformation is a rather generic type, and both Information Maps and Reports use that as a base type so we'll need to distinguish our new Widget type from them. We'll use a TransformRole of "Widget" to trigger creation of our class, so the filter would be "@TranformRole='Widget'".

Once your configuration is in place, make sure your class is in a jar file or directory that's searched by your application's class loader, and then you're ready to try it out. If you perform an Information Service operation that returns an object of that type, you should get an instance of your new class back.

## Custom Filters

Now that you have a class that represents your data, you'll probably want a way to easily find that kind of data. There is a generic filtering capability in the Information Service, but by creating a custom filter for your type, you can provide more intuitive and powerful searching capbilities.

It's difficult to create a generic filter that hides model differences. Different repository types will generally use different type names, attribute name, even different query languages. However, a type-specific filter class can easily accomodate these differences and hide them from the client application. The first thing to consider is what attributes are likely to be useful to search on. If you want to search on keywords, you'll want to subclass the `com.sas.services.information.metadata.KeywordFilter` class. This will give you the keyword add/remove functionality. It is still up to your filter class to generate the correct search syntax for your repository(s), though. Each repository type will make a different call to get a search string that it can use. OMI repositories will call `getXMLSelectString()`, and LDAP repositories will call `getLdapString()`. Your filter class has to do the work of creating those strings based on the set search criteria and returning it to the repository. Besides an XMLSelect string, your filter can also set a template that the Repository will use to retrieve your data. Templates can do some filtering as well as control the data that's returned, so it may be necessary to set the template depending on what search criteria was set.