Create ITIM TDI DSML Based Service with a script

The following code creates a DSML based adapter for TIM 4.6 in a jar via template manipulations. It is here for reference on how to mess with the templates. A better win batch based code that builds RMI adapters for TIM 5.0+ is here

#!/bin/perl
#
# ITIM Service Modeler v1.2 (c) 2006 Alex Ivkin
#
use XML::Parser;
use strict;

my $instanceID = 1; # Instance ID for object identifier in the schema file
# $/="\0x0d\0x0a"; - wont work

my $IDISERVICE_TEMPLATE="IDIServiceTemplate.xml"; # IDI Template for service processing
my $OID_DIRECTORY="OIDDirectory.oid";  #  agent id file format - 'name - number' used for OIDs\n  Existing files are going to be overwritten\n";

my $ACCESS360_OID="1.3.6.1.4.1.6054";
my $LDAPSTRING_OID="1.3.6.1.4.1.1466.115.121.1.15";

if (!defined(@ARGV) || $#ARGV < 1){
    print <<"EOH";
Usage: $0 <service name> <service type>
 $0 expects the following files in the directory it is run from
    OIDDirectory.oid - contains services OID directory in the following format - Service Name,Service Number
   IDIDBServiceTemplate.xml - contains a template for an IDI service with magic words in appropriate places
   <service>.conf - contains magic words with a real value for the service in the following format - %%WORD%% realvalue
   <service>.csv - contains a list of fields to be queried and provisioned via an IDI service in the following format - Field Name, Field Description
   Make sure <service> does not contain underscores if running on TIM with IDS - they are unsupported by IDS.

   <service type> must be one of the following: LDAP or SQL, depending on a type of backend

   Note: keep the service name short if using IDS for TIM. The LDAP fields are created by appending a field name to a service name. Due to a DB2 limitation under IDS as of v6.0 they are shortened to 16 symbols and thus should be unique within the first 16 symbols. If the service name is too long the combined field names might appear the same to IDS!
EOH
    exit(1);
}
my ($serviceName)=split('\.',shift);
my $serviceType=shift;
if ((lc($serviceType) ne "ldap") && (lc($serviceType) ne "sql")){
   print "unknown service type\n";
   exit(1);
}

open(CSV_FILE, "$serviceName.csv") || die "Can't open file: $serviceName.csv $!";
open(IDITEMPLATE_FILE, $IDISERVICE_TEMPLATE) || die "Can't open file: $IDISERVICE_TEMPLATE $!";

my $serviceOID=get_service_oid($serviceName);
my %serviceCFG=load_conf($serviceName);

# create the output files
my $serviceFolder=$serviceName."Service\\";
my $serviceNameService=$serviceName."Service";
my $serviceNameAccount=$serviceName."Account";
my $serviceNameServiceClass="er".$serviceNameService;
my $serviceNameAccountClass="er".$serviceNameAccount;
mkdir $serviceFolder;
open(ACCOUNT_XML_FILE,  ">:crlf",$serviceFolder."er".$serviceName."Account.xml") || die "Can't create file: >".$serviceName."Service\\".$serviceName."account.xml - $!";# Open xml file for output
open(SERVICE_XML_FILE,  ">:crlf",$serviceFolder."er".$serviceName."Service.xml") || die "Can't create file: $!"; # Open xml file for output
open(PROP_FILE,     ">:crlf",$serviceFolder."CustomLabels.properties") || die "Can't open file: $!";# Open properties file for output
open(RES_FILE,      ">:crlf",$serviceFolder."resource.def") || die "Can't open file: $!";# Open resource file for output
open(SCHEMA_FILE,   ">:crlf",$serviceFolder."schema.dsml") || die "Can't open file: $!";# Open schema file for output
open(XFORMS_FILE,   ">:crlf",$serviceFolder."xforms.xml") || die "Can't open file: $!";# Open schema file for output
open(IDISERVICE_FILE,   ">:crlf",$serviceName."IDIService.xml") || die "Can't open file: $!";# Open schema file for output
#open(LDAPCLEANER_FILE,   ">:crlf",$serviceName."schemacleaner.ldif") || die "Can't open file: $!";# Open schema file for output

write_prefixes();
# --------------------------------------------
my $account_schema="";
my $idi_input_schema="";
my $idi_input_map="";
my $idi_output_schema="";
my $idi_output_map="";
my ($idi_objectclass_id, $idi_special);
$idi_special.="<AttributeMapItem><Name>\$dn</Name><Type>advanced</Type><Enabled>true</Enabled><Add>true</Add><Modify>true</Modify>\n";
$idi_special.=qq|<Script><![CDATA[ret.value="eruid="+(conn.getString("$serviceCFG{"%%UNIQUE_ID%%"}")).trim().replace(",",".");]]></Script>\n|;
$idi_special.="<Simple>\$dn</Simple></AttributeMapItem>\n";
$idi_special.="<AttributeMapItem><Name>eruid</Name><Type>advanced</Type><Enabled>true</Enabled><Add>true</Add><Modify>true</Modify>\n";
$idi_special.=qq|<Script><![CDATA[var uid=conn.getString("$serviceCFG{"%%UNIQUE_ID%%"}").trim();\nif (uid.length()==0)\n    ret.value="Unnamed Account";\nelse \n   ret.value=uid;]]></Script>\n|;
$idi_special.="<Simple>eruid</Simple></AttributeMapItem>\n";
# see if we have the password field defined as well
if (defined($serviceCFG{"%%ACCOUNT_PWD%%"})){
   $idi_special.="<AttributeMapItem><Name>erpassword</Name><Type>advanced</Type><Enabled>true</Enabled><Add>true</Add><Modify>true</Modify>\n";
   $idi_special.=qq|<Script><![CDATA[var pwd=conn.getString("$serviceCFG{"%%ACCOUNT_PWD%%"}").trim();\nif (pwd.length()==0)\n    ret.value="changeme";\nelse \n   ret.value=pwd;]]></Script>\n|;
   $idi_special.="<Simple>erpassword</Simple></AttributeMapItem>\n";
}

$idi_objectclass_id="<AttributeMapItem><Name>objectclass</Name><Type>advanced</Type><Enabled>true</Enabled><Add>true</Add><Modify>true</Modify>\n";
$idi_objectclass_id.="<Script><![CDATA[ret.value=\"$serviceNameAccountClass\";]]></Script>\n";
$idi_objectclass_id.="<Simple>objectclass</Simple></AttributeMapItem>\n";

$idi_input_map.=$idi_special.$idi_objectclass_id;
#$idi_output_map.=$idi_unique_id.$idi_objectclass_id;

# start with special attributes
$account_schema.=qq{       <attribute ref="erUid" required="true"/>\n};
if (defined($serviceCFG{"%%ACCOUNT_PWD%%"})){
   $account_schema.=qq{       <attribute ref="erPassword" required="false"/>\n};
}
my @field;
# While loop to parse csv file - write the meat of the files
while(<CSV_FILE>) {
   s/\s+$//; # Delete new line character - dont use chomp - it will delete only one character instead of two
#  next if ($. == 1); # skip the first line
  /,/ ? @field = /^(.+?),(.*)/ : $field[0]=$_; # Split the fields if there is a comma
  # change _ to - because IDS DOES NOT SUPPORT UNDERSCORES in attribute names
  my $orig_field=$field[0];
  $field[0]=~s/[_ ]/-/g;
  $field[0]=~s/[()"]//g;
  # get the quotes out
  $field[1]=~s/"//g;
  # Output to xml file
 # print ACCOUNT_XML_FILE qq{<formElement name="data.$serviceName$field[0]" label="\$$serviceName$field[0]"><input name="data.$serviceName$field[0]" size="" type="text"/></formElement>\n};
  # build account xml, skipping the uid line since it is taken care of by eruid at the beginning of the file (actual mapping is done inside of IDI)
  if ((!($orig_field eq $serviceCFG{"%%UNIQUE_ID%%"})) and (!defined($serviceCFG{"%%ACCOUNT_PWD%%"}) or !($orig_field eq $serviceCFG{"%%ACCOUNT_PWD%%"}))){
   print ACCOUNT_XML_FILE "<formElement name=\"data.".lc("$serviceName$field[0]").qq{" label="\$$serviceName$field[0]"><input name="data.}.lc("$serviceName$field[0]").qq{" size="" type="text"/></formElement>\n};
  }
  # Output to properties file
  # Makes variable lowercase
#  my $lcfield = lc($field[0]);
#  print PROP_FILE "$lcfield=".(length($field[1])==0?$field[0]:$field[1])."($field[0])\n";
  print PROP_FILE "$serviceName$field[0]=".(length($field[1])==0?$field[0]:"$field[0] ($field[1])")."\n";
  $field[1]=$field[0] if (length($field[1])==0);
  # Output attribute definitions to schema file
  print SCHEMA_FILE<<"EOF";
    <attribute-type single-value = "true">
        <name>$serviceName$field[0]</name>
        <description>$field[1]</description>
        <object-identifier>$ACCESS360_OID.3.$serviceOID.2.$instanceID</object-identifier>
        <syntax>$LDAPSTRING_OID</syntax>
    </attribute-type>
EOF

# print LDAPCLEANER_FILE $ACCESS360_OID.3.$serviceOID.2.$instanceID;

   print XFORMS_FILE qq{<EnRoleAttribute Name="$serviceName$field[0]" RemoteName="$orig_field" RemoteAccess="W" RemoteRDN="true"/>\n};

   # build schema
   $account_schema.="       <attribute ref=\"$serviceName$field[0]\" required=\"false\"/>\n";

   # build idi input feed
   $idi_input_schema.=  "<SchemaItem><Name>$orig_field</Name><Presence>null</Presence></SchemaItem>\n";
   $idi_input_map.= "<AttributeMapItem><Name>$serviceName$field[0]</Name><Type>simple</Type><Enabled>true</Enabled><Add>true</Add><Modify>true</Modify><Script></Script><Simple>$orig_field</Simple></AttributeMapItem>\n";
   # and idi output feed
   $idi_output_schema.= "<SchemaItem><Name>$orig_field</Name><Presence>null</Presence></SchemaItem>\n";

   # we need special processing for the password
   if (defined($serviceCFG{"%%ACCOUNT_PWD%%"}) and $orig_field eq $serviceCFG{"%%ACCOUNT_PWD%%"}) {
        # do the same for the password if defined
        $idi_output_map.= qq|<AttributeMapItem><Name>$orig_field</Name><Type>advanced</Type><Enabled>true</Enabled><Add>true</Add><Modify>true</Modify><Script>|;
        $idi_output_map.= qq|<![CDATA[dec = new Packages.com.ibm.di.util.Base64EntryConverter("passwd");\ne = system.newEntry();\ne.setAttribute("passwd", work.getAttribute("erpassword"));\ndec.base64decode(e);\nret.value=system.arrayToString(e.getObject("passwd"));]]>|;
        $idi_output_map.= qq|</Script><Simple>erpassword</Simple></AttributeMapItem>\n|;
   } else {
        $idi_output_map.= qq|<AttributeMapItem><Name>$orig_field</Name><Type>simple</Type><Enabled>true</Enabled><Add>true</Add><Modify>true</Modify><Script></Script><Simple>|;
        # proper mapping for the UID
        if ($orig_field eq $serviceCFG{"%%UNIQUE_ID%%"}){
           $idi_output_map.= "eruid";
        } else {
           $idi_output_map.= $serviceName.$field[0];
        }
        $idi_output_map.=qq|</Simple></AttributeMapItem>\n|;
   }


   # move on to the next ldap element
   $instanceID++;
}
# Print out service class definitions an account class header lines to schema file
print SCHEMA_FILE<<"EOF";
  <!--************************************************************-->
  <!--class definitions                                           -->
  <!--************************************************************-->
  <class superior="top">
        <name>$serviceNameServiceClass</name>
        <description>Class representing $serviceName DSML2 service</description>
        <object-identifier>$ACCESS360_OID.3.$serviceOID.1.1</object-identifier>
        <attribute ref="erservicename" required="true"/>
        <attribute ref="description" required="false"/>
        <attribute ref="erurl" required="true"/>
        <attribute ref="eruid" required="false"/>
        <attribute ref="erpassword" required="false"/>
        <attribute ref="namingcontexts" required="true"/>
        <attribute ref="ercategory" required="false"/>
    </class>
  <class superior="top">
    <name>$serviceNameAccountClass</name>
    <description>Class representing $serviceName DSML2 account</description>
    <object-identifier>$ACCESS360_OID.3.$serviceOID.1.2</object-identifier>
EOF
print SCHEMA_FILE $account_schema;
# create the res file
create_res_file();
# --------------------------------------------
# Print closing lines of schema and close schema file
write_suffixes();
# Print resource.def file -> needs to be taylored
close ACCOUNT_XML_FILE;
close SERVICE_XML_FILE;
close PROP_FILE;
close SCHEMA_FILE;
close CSV_FILE;
close RES_FILE;

# now lets build the IDI service
#
build_idi_service();

# zip up everything
#print "jar cvf ".$serviceNameService.".jar ".$serviceNameService."/";
my $zipitup = system("jar cvf ".$serviceNameService.".jar ".$serviceNameService."/");
if ($zipitup) {
    print "Jar.exe is not found or had an error. \n";
} else {
    `rm -r $serviceNameService`;
}

print "done.\n";


sub get_service_oid {
    # parse oid to find the service OID
    my $service=shift;
    my $max_oid=0;
    my $serviceOID=0;
    open(OID_FILE, $OID_DIRECTORY) || die "Can't open file: $OID_DIRECTORY $!";
    while (<OID_FILE>){
        s/\s+$//; # Delete new line character - dont use chomp - it will delete only one character instead of two
        my ($service,$oid)=split(':');
        if ($service eq $serviceName){
            $serviceOID=$oid;
        } else {
            if ($max_oid < $oid){
                $max_oid=$oid;
            }
        }
    }
    if ($serviceOID == 0){
        $serviceOID=$max_oid+1;
        print "Service OID not found. A new OID is created for $serviceName - ".$ACCESS360_OID.".3.".$serviceOID."\n";
        close OID_FILE;
        open(OID_FILE, ">>$OID_DIRECTORY") || die "Can't open file: $OID_DIRECTORY $!"; # Open csv file for input
        print OID_FILE $serviceName.":".$serviceOID."\n";
    } else {
        print "Service OID found. $serviceName is ".$ACCESS360_OID.".3.".$serviceOID."\n";
    }
    close OID_FILE;
    return $serviceOID;
}

sub load_conf {
    my $serviceName=shift;
    my %serviceCFG;
    open(SERVICE_CONF, "$serviceName.conf") || die "Can't open file: $serviceName.conf $!"; #
    while(<SERVICE_CONF>){
        s/\s+$//; # Delete new line character - dont use chomp - it will delete only one character instead of two
        next if /^#.*/; # skip comments
        my ($keyword,$string)=/(.+)\s+(.+)\s*/;
        $serviceCFG{$keyword}=$string;
    }
    close SERVICE_CONF;
    return %serviceCFG;
}

sub write_prefixes {
    # Print opening lines for XML file
    print ACCOUNT_XML_FILE "<page>\n<body>\n<form action=\"formvalidator0\">\n";
    print SERVICE_XML_FILE "<page>\n<body>\n<form action=\"formvalidator0\">\n";
    # Print opening lines of schema file
    print SCHEMA_FILE "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n";
    print SCHEMA_FILE "<dsml>\n\n";
    print SCHEMA_FILE " <directory-schema>\n\n";
    print SCHEMA_FILE "  <!--************************************************************-->\n";
    print SCHEMA_FILE "  <!--attribute definitions                                       -->\n";
    print SCHEMA_FILE "  <!--************************************************************-->\n";

    print XFORMS_FILE qq(<?xml version="1.0" encoding="UTF-8"?>\n<EnRoleTransformations>\n);

    # prefix account form with special fields
    print ACCOUNT_XML_FILE qq{<formElement name="data.eruid" label="\$eruid" required="true"><input name="data.eruid" size="" type="text"/><constraint><type>REQUIRED</type><parameter>true</parameter></constraint></formElement>\n};
    if (defined($serviceCFG{"%%ACCOUNT_PWD%%"})){
       print ACCOUNT_XML_FILE qq{<formElement name="data.erpassword" label="\$erpassword" required="false"><input name="data.erpassword" size="" type="text"/></formElement>\n};
    }

    print SERVICE_XML_FILE << "EOF";
          <formElement name="data.erservicename" label="\$erservicename" required="true"><input name="data.erservicename" size="50" type="text"/><constraint><type>REQUIRED</type><parameter>true</parameter></constraint></formElement>
          <formElement name="data.description" label="\$description"><input name="data.description" size="50" type="text"/></formElement>
          <formElement name="data.erurl" label="\$erurl" required="true"><input name="data.eruid" size="50" type="text"/><constraint><type>REQUIRED</type><parameter>true</parameter></constraint></formElement>
          <formElement name="data.eruid" label="\$eruid"><input name="data.eruid" size="50" type="text"/></formElement>
          <formElement name="data.erpassword" label="\$erpassword"><input name="data.erpassword" size="50" type="password"/></formElement>
          <formElement name="data.namingcontexts" label="\$namingcontexts" required="true"><input name="data.namingcontexts" size="50" type="text"/><constraint><type>REQUIRED</type><parameter>true</parameter></constraint></formElement>
          <formElement name="data.ercategory" label="\$ercategory"><input name="data.ercategory" size="50" type="text"/></formElement>
          <formElement name="data.owner" label="\$owner"><searchControl operator="1" type="input" attribute="cn" orgSubTreeSearch="true" category="Person" class="Person"><comboItem></comboItem></searchControl></formElement>
          <formElement name="data.erprerequisite" label="\$erprerequisite"><searchControl operator="1" type="input" attribute="erservicename" orgSubTreeSearch="true" category="Service" class="DSMLInfo"><comboItem></comboItem></searchControl></formElement>
EOF
}

sub write_suffixes {
#   print SCHEMA_FILE qq{       <attribute ref="description" required="false"/>\n};
    print SCHEMA_FILE "</class>\n</directory-schema>\n</dsml>\n";
    # Print closing xml lines and close xml file
    #print ACCOUNT_XML_FILE qq{"<subForm height="300" width="800"><property name="customServletURI">logon</property></subForm></formElement></form>\n</body>\n</page>"};
    print ACCOUNT_XML_FILE "</form>\n</body>\n</page>\n";
    print SERVICE_XML_FILE "</form>\n</body>\n</page>\n";
    print XFORMS_FILE "</EnRoleTransformations>\n";
}

sub create_res_file {
    print RES_FILE<<"EOF";
<?xml version="1.0" encoding="UTF-8"?>
<Resource>
 <!--The system profile contains an overall description and any specific-->
 <!--properties to be used for communications.-->
 <SystemProfile>
    <Name>$serviceNameService</Name>
    <Description>$serviceName service.</Description>
    <BehaviorProperties>
     <!--The service provider factory should have the value used to-->
     <!--instantiate the DSML2 protocol module.-->
     <Property Name = "com.ibm.itim.remoteservices.ResourceProperties.SERVICE_PROVIDER_FACTORY" Value="com.ibm.itim.remoteservices.provider.dsml2.DSML2ServiceProviderFactory"/>
<!--     <Property Name  = "com.ibm.itim.remoteservices.ResourceProperties.TRANSFORMER" Value = "com.ibm.itim.remoteservices.transformation.EnroleAgentMessageTransformer"/>-->
<!--     <Property Name  = "com.ibm.itim.remoteservices.ResourceProperties.TRANSFORM_FILE_NAME"  Value = "xforms.xml" />-->
    </BehaviorProperties>
 </SystemProfile>

 <!--Protocol properties add values from the service instance to request messages to the -->
 <!--end point.-->
 <ProtocolProperties>
      <Property Name  = "url" LDAPName = "erurl"/>
      <Property Name  = "principal" LDAPName = "erUid" />
      <Property Name  = "credentials" LDAPName = "erPassword" />
<!--        <Property Name = "eruid" LDAPName="eruid"/>-->
<!--        <Property Name = "erurl" LDAPName="erurl"/>-->
 </ProtocolProperties>

 <!--Defines a profile for the custom account type.-->
 <AccountDefinition ClassName = "$serviceNameAccountClass"
                        Description="$serviceName user account.">
 </AccountDefinition>

 <!--Defines a profile for the custom service type.-->
 <ServiceDefinition ServiceProfileName = "$serviceNameService"
    ServiceClass = "$serviceNameServiceClass"
        AttributeName="erServiceName"
        AccountClass="$serviceNameAccountClass"
        AccountProfileName="$serviceNameAccount"
        Description="$serviceName service.">
 </ServiceDefinition>
</Resource>
EOF
}

sub build_idi_service(){
    my ($k,$v);
    # add predefined entries for the assembly line attributes
    $serviceCFG{"%%INPUT_SCHEMA%%"}=$idi_input_schema;
    $serviceCFG{"%%INPUT_ATTRIBUTE_MAP%%"}=$idi_input_map;
    $serviceCFG{"%%OUTPUT_SCHEMA%%"}=$idi_output_schema;
    $serviceCFG{"%%OUTPUT_ATTRIBUTE_MAP%%"}=$idi_output_map;
    # change the  attributes map for the update assembly line
    $idi_output_map =~ s|<Add>true</Add>|<Add>false</Add>|g;
    $serviceCFG{"%%OUTPUT_ATTRIBUTE_MAP_FOR_UPDATES%%"}=$idi_output_map;
    # filter out incorrect values for %%EH_USESSL%% 
    $serviceCFG{"%%EH_USESSL%%"} = "false" if (!defined($serviceCFG{"%%EH_USESSL%%"}) or !(lc($serviceCFG{"%%EH_USESSL%%"}) eq "true"));
    # pick a service type
    if (lc($serviceType) eq "sql"){
      $serviceCFG{"%%CONNECTOR_TYPE%%"}="databaseConnector";
      $serviceCFG{"%%CONNECTOR_INHERITANCE%%"}="/Connectors/databaseConnector";
    } else {
      $serviceCFG{"%%CONNECTOR_TYPE%%"}="ldapConnector";
      $serviceCFG{"%%CONNECTOR_INHERITANCE%%"}="/Connectors/ldapConnector";
    }

    while(<IDITEMPLATE_FILE>){
        s/\s+$//; # Delete new line character - dont use chomp - it will delete only one character instead of two
        s/$k/$v/g while (($k,$v)=each(%serviceCFG)); # replace all keywords in the current line with their values
        print IDISERVICE_FILE "$_\n";
    }
    close IDISERVICE_TEMPLATE;
    close IDISERVICE_FILE;
    close XFORMS_FILE;
}

@Tools @ITIM