I was watching this presentation ( Getting started with the SailPoint Rule Development Kit ) and thought I would share some changes that I’ve made to the RDK to make my life easier.
Two things bothered me that I could solve:
- I don’t like having duplicated code, e.g. rule-development-kit/src/main/java/UsernameGenerator.java at a4fdf22221b1d5a0cd8f4f4f9db414c511c4b84a · sailpoint-oss/rule-development-kit · GitHub and rule-development-kit/src/main/resources/rules/Rule - AttributeGenerator - UsernameGenerator.xml at a4fdf22221b1d5a0cd8f4f4f9db414c511c4b84a · sailpoint-oss/rule-development-kit · GitHub . They are mostly the same, but not quite.
We have to manually copy Java code into the XML file, remove some imports, but not all, add some code outside of the Java functions (the BSH entry), and do some other small changes (e.g. remove objects that are in context already). This has to be done over and over to keep the files in sync and it is very error prone.
- You could just edit only the XML files, without having any Java code, but this doesn’t offer the same flexibility. You can see that that the IDE support is not that good, so typos are not caught automatically by the IDE. JUnit test for the helper functions are also easier to understand if they don’t have any beanshell loading, so you need to have Java code and XML code.
My solution was to use a plugin to generate the XML code from the Java classes at compile time.
Using the plugin com.igormaznitsa:jcp
in pom.xml
:
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>com.igormaznitsa</groupId>
<artifactId>jcp</artifactId>
<version>7.1.1</version>
<executions>
<execution>
<id>preprocessSources</id>
<phase>generate-sources</phase>
<goals>
<goal>preprocess</goal>
</goals>
<configuration>
<sources>
<source>${basedir}/src/main/java/rules</source>
</sources>
<target>${project.basedir}/src/main/resources/rules</target>
<!-- <keepComments>REMOVE_JCP_ONLY</keepComments> -->
<keepComments>false</keepComments>
<keepLines>false</keepLines>
<dontOverwriteSameContent>true</dontOverwriteSameContent>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
and creating the Java class like this:
//#outname "Rule - AttributeGenerator - UserPrincipalNameGenerator.xml"
//$$<?xml version='1.0' encoding='UTF-8'?>
//$$<!DOCTYPE Rule PUBLIC "sailpoint.dtd" "sailpoint.dtd">
//$$<Rule name="UserPrincipalNameGenerator" type="AttributeGenerator">
//$$ <Description>Generate a unique UserPrincipalName for Active Directory.</Description>
//$$ <Source><![CDATA[
//#//
package rules;
import java.text.Normalizer;
import java.text.Normalizer.Form;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.WordUtils;
//#//
import org.apache.log4j.LogManager;
//#//
import org.apache.log4j.Logger;
import sailpoint.object.Application;
import sailpoint.object.Identity;
import sailpoint.server.IdnRuleUtil;
//#if false
/**
* ...
*/
@SuppressWarnings({"rawtypes", "FieldMayBeFinal"}) // BeanShell doesn't support generics
public class UserPrincipalNameGenerator {
Logger log = LogManager.getLogger(rules.UserPrincipalNameGenerator.class);
Identity identity = new Identity();
Application application = new Application();
IdnRuleUtil idn;
//#endif
private static String RULE_CONFIG_ATTRIBUTE = "userPrincipalNameGeneratorConfig";
private static int MAX_USERNAME_LENGTH = 64;
public void loadConfiguration() {
...
}
public String generateUserPrincipalName(String firstName, String lastName, String domain) {
...
}
String generateUserPrincipalName(String firstName, String lastName, String domain, int maxUsernameLength) {
...
}
String getCanonicalFirstName(String firstName) {
...
}
String getCanonicalLastName(String lastName) {
...
}
private static String capitalize(String lastName) {
...
}
private static String replaceUnsupportedCharsWithSpace(String firstName) {
...
}
private Map ASCII_REPLACEMENTS = Map.of(
...
);
private String normalizeDiacritics(String text) {
...
}
public boolean isUpnUnique(String upn) {
...
}
String getDomain(Identity identity) {
...
}
//#//
String beanshellEntry() {
loadConfiguration();
String domain = getDomain(identity);
if (domain == null) {
return null;
}
return generateUserPrincipalName(identity.getFirstname(), identity.getLastname(), domain);
//#//
}
//#//
}
//$$ ]]></Source>
//$$</Rule>
This allows to exclude some lines from XML (e.g. importing of org.apache.log4j.* classes, the beanshell entry function declaration), or blocks of code (documentation, class declaration and objects already present in context - log, identity, idn, application).
This code can be than broken in individual functions which can be easily tested independently, without having to set up identities or the BeanShell environment.
For my personal taste I would also try to reduce the duplicate code in the tests, I find such tests easier to read than rule-development-kit/src/test/java/sailpoint/UsernameGeneratorTest.java at a4fdf22221b1d5a0cd8f4f4f9db414c511c4b84a · sailpoint-oss/rule-development-kit · GitHub
@Test
public void testRule_uniqueLogic() {
mockIdentity("John", "Doe");
mockAccountExists("[email protected]");
mockAccountExists("[email protected]");
assertEquals("[email protected]", evalRule());
}
Next step would be to use AssertJ as testing library instead of the plain JUnit.