Generic Max Length Validation using Hibernate Metadata

Introduction

Within IdentityIQ, it is common to validate the length of input values to ensure they do not exceed the defined maximum length in the corresponding database table. This validation applies to standard object attributes, extended attributes, and named column attributes.

Although implementing this input validation is straightforward, the task becomes increasingly challenging to maintain as the number of custom attributes in the object class grows.

To address this challenge, we propose using Hibernate mapping as the master record for column maximum length validation for each SailPointObject class. By doing so, we can develop a generic validation function specifically designed to check column lengths efficiently and effectively.

Where to find Hibernate Mapping

hibernate.cfg.xml

All default hibernate mapping configurartion are defined in the hibernate.cfg.xml and {SailPointObjectClass}.hbm.xml files. There are all shipped within ~/WEB-INF/lib/identityiq.jar. If we look closer in the hibernate.cfg.xml, we notice that it maps additional configuration per each object, for example:

<mapping resource="sailpoint/object/Application.hbm.xml"/>
<mapping resource="sailpoint/object/Bundle.hbm.xml"/>

We then look closer to Bundle.hbm.xml, we will find the clear hibernate mapping configuration for the sailpoint.object.Bundle object.

<hibernate-mapping>
<class name="sailpoint.object.Bundle">
 
    &SailPointObject;
&BundleExtensions;
&Classifiable;
 
    <property name="name" type="string" length="128" unique="true" not-null="true"/>
    <property name="displayName" type="string" length="128"/>
    <property name="displayableName" type="string" length="128" index="spt_bundle_dispname_ci"/>
    <property name="disabled" type="boolean" index="spt_bundle_disabled"/>
....
</class>
</hibernate-mapping>

Extended Attributes

In addition, following by the post Managing Extended Attributes , we can also define extended attributes with dedicated column within the corresponding database table, which specified under [IdentityIQ installation directory]/WEB-INF/classes/sailpoint/object directory and are called [object]Extended.hbm.xml, example property entry:

<property name="costCenter" type="string" length="450"
            access="sailpoint.persistence.ExtendedPropertyAccessor"
                        index="spt_identity_cost_center_ci"/>

Our objective here is to load those hibernate mapping metadata programmically and use it for the string max length validation.

Retrieve Hibernate Mapping

Commonly, in java we can use StandardServiceRegistryBuilder to load the hibernate.cfg.xml, then retrieve the metadata of hibernate mapping, see the sample code below:

import org.hibernate.SessionFactory;
import org.hibernate.boot.Metadata;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.mapping.Column;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.mapping.Property;

public class MetadataRetrieval {
    public static void main(String[] args) {
        SessionFactory sessionFactory = new StandardServiceRegistryBuilder().configure("hibernate.cfg.xml").build();
        Metadata metadata = new MetadataSources(sessionFactory).buildMetadata();

        PersistentClass persistentClass = metadata.getEntityBinding("com.example.entity.User");
        if (persistentClass != null) {
            for (Property property : persistentClass.getPropertyIterator()) {
                for (Column column : property.getValue().getColumnIterator()) {
                    System.out.println("Property: " + property.getName());
                    System.out.println("Column: " + column.getName());
                    System.out.println("Column Length: " + column.getLength());
                }
            }
        }

        sessionFactory.close();
    }
}

Within the Tomcat application hibernate.cfg.xml is accessible as long as its under the application CLASS_PATH.

However, this function may not work well within the IdentityIQ application because the default hibernate.cfg.xml does not contain the necessary settings such as dataSource.username, dataSource.password, and Hibernate dialects, among others. While we could load these settings additionally, we found a helpful class: sailpoint.persistence.HibernateMetadataIntegrator. Using this helper class, we can easily retrieve the Hibernate mapping metadata.

In the example below, we construct a method that returns a map where the key is the attribute, and the value is the maximum length defined in the Hibernate mapping.

public static Map<String, Integer> getHibernateMetadata(SailPointContext context, SailPointObject spObj) throws GeneralException {
	Map<String, Integer> map = new HashMap<>();
	// Create Metadata
	Metadata metadata = HibernateMetadataIntegrator.INSTANCE.getMetadata();
	// Retrieve PersistentClass for the entity
	PersistentClass persistentClass = metadata.getEntityBinding(spObj.getClass().getName());
	if (persistentClass != null) {
		@SuppressWarnings("unchecked")
		Iterator<Property> it = (Iterator<Property>) persistentClass.getPropertyClosureIterator();
		while (it.hasNext()) {
			Property property = it.next();
			String propertyName = property.getName();
			if (!"string".equalsIgnoreCase(property.getType().getName())) { // skip property which type is not string
				continue;
			}
			@SuppressWarnings("unchecked")
			Iterator<Column> it2 = (Iterator<Column>) property.getColumnIterator();
			while (it2.hasNext()) {
				Column column = it2.next();
				int maxLength = column.getLength();
				map.put(propertyName, maxLength);
				break;
			}
			Util.flushIterator(it2);
		}
		Util.flushIterator(it);
	}
	transformHibernateMapping(context, spObj, map); // this method is to handle attributes which use default extended columns
	return map;
}

As the initial propertyName also includes the default extended* columns, and we then use the function transformHibernateMapping to map the attributes with its corresponding extended column in the map by replacing the key value. Then remove all unused extended* column from the map.

private static void transformHibernateMapping(SailPointContext context, SailPointObject spObj,
		Map<String, Integer> map) throws GeneralException {
	// transform extended attributes into corresponding attribute name
	ObjectConfig objConfig = context.getObjectByName(ObjectConfig.class, spObj.getClass().getSimpleName());
	if (objConfig != null) {
		List<ObjectAttribute> objAttrs = objConfig.getExtendedAttributeList();
		for (ObjectAttribute objAttr : Util.safeIterable(objAttrs)) {
			if (objAttr.isExtended()) {
				if (objAttr.getType() == null || "string".equalsIgnoreCase(objAttr.getType())) {
					String extendedAttribute = "extended" + Util.otos(objAttr.getExtendedNumber());
					if (map.containsKey(extendedAttribute)) {
						// replace the used extended column key
						int value = map.remove(extendedAttribute);
						map.put(objAttr.getName(), value);
					}
				}
			}
		}
	}
	// remove unused extended* entries 
	map = map.entrySet()
			.stream()
			.filter(entry -> !entry.getKey().startsWith("extended"))
			.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}

As of now, we have obtained a map that contains the string length limits for the provided SailPointObject class. This method is designed to be generic and can be called for all SailPointObject classes. We would also like to make the validator class generic for the provided SailPointObject, ensuring that the validation is independent of their attribute sets.

public static List<String> validateSailPointObjectColumnLength(SailPointContext context, SailPointObject spObj) {
	List<String> validations = new ArrayList<>();
	try {
		Map<String, Integer> map = getHibernateMappingStringLimit(context, spObj);
		for (String attr : map.keySet()) {
			String value = (String) getAttributeWithJavaReflection(spObj, attr);
			// validate string value length
			if (Util.isNotNullOrEmpty(value)) {
				int limit = map.get(attr);
				int valueLength = value.length();
				if (valueLength > limit) {
					validations.add(attr + " exceed limit (" + limit + "), while length is " + valueLength);
				}
			}
		}
	} catch (Exception ex) {
		ex.printStackTrace();
	}
	return validations;
}

public static Object getAttributeWithJavaReflection(SailPointObject spObj, String attr) throws GeneralException {
	Object value = null;
	try {
		value = getValueUsingAPIMethod(spObj, attr);
	} catch (NoSuchMethodException e) {
		// now try use getAttribute method,
		try {
			Method method = spObj.getClass().getMethod("getAttribute", String.class);
			value = method.invoke(spObj, attr);
		} catch (NoSuchMethodException e1) {
			// spObject has no getAttribute method
		} catch (Exception ex) {
			throw new GeneralException(ex.getMessage());
		}
	} catch (GeneralException e) {
		throw new GeneralException("failed to get attribute: " + attr, e);
	}
	return value;
}

public static Object getValueUsingAPIMethod(SailPointObject spObj, String attr) throws GeneralException, NoSuchMethodException {
	String methodPrefix = "get";
	Method method;
	try {
		String camelCaseAttr = attr.substring(0, 1).toUpperCase() + attr.substring(1);
		String methodName = methodPrefix + camelCaseAttr;
		method = spObj.getClass().getMethod(methodName);
		return (Object) method.invoke(spObj);
	} catch (SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
		throw new GeneralException("failed getValueUsingAPIMethod for attribute " + attr, e);
	}
}

Here we use Java Reflection to make the validateSailPointObjectColumnLength generic to all SailPoint object class. The getAttributeWithJavaReflection will first try to use get method api to retrieve the value, e.g. name attribute will first be called with getName(), if the getter method is not found, then it will try to use getAttribute() as second alternative to retrieve the corresponding value.

We then compare the string value length with the hibernate mapping metadata we got previously.

Note: description attribute does not exist directly in the SailPointObject class; it is typically stored within the Attributes map. The string length limitation for the description field is defined in the LocalizedAttribute class and is set to 1024 characters by default. It’s suggested to checked the string length in the a caller function.

Conclusion

In this blog, we demonstrate how to retrieve Hibernate mapping metadata and use it as a master record for string length validation. This approach is not limited to a specific SailPointObject class but can be applied to all SailPointObject classes.

In real-life scenarios, you might want to create a parent method that is more specific to a particular object class (e.g., Bundle) and apply additional checks for specific business logic.

The purpose of using Hibernate mapping metadata for string length validation is to support the maintenance of potentially large codebases, especially when custom attributes are constantly being introduced. We hope this method may assist you in your implementations in some way.

2 Likes