Skip to main content

Customizing uPortal (Part 3)

Strategy Pattern

In our previous post in the series, we touched on some basics of Spring Framework and gave a real example of using the Decorator pattern to customize uPortal 5. In this post we continue our journey of leveraging design patterns for uPortal 5 customization.

Strategy

From our first post, Strategy pattern is a pattern that encapsulates more than one implementation of some feature and makes these implementations interchangeable. How is this done in practice? Let us continue with another real example from uPortal.

On a recent effort to remediate the SmartLDAP feature of uPortal, I discovered some changes that made assumptions on child group keys. This held true for the author of the change, but this change was incompatible with some existing installations that used SmartLDAP. The default should really be to accept the child group key without processing. Some deployers, like the author of the change, should be able to easily configure uPortal to use their key modification code.

To be clear, this example and the Decorator example in the previous post were not written as examples for this write-up. While the previous example really matched the Decorator pattern, this example comes close but does not completely match the Strategy pattern (I will come back to this discussion point later).

The class that required the refactor is org.apereo.portal.groups.smartldap.SmartLdapGroupStore.java in the uPortal repo. In the method, buildGroupsTree(), the code that needed to be optional was:

```
                 final int keyStart = childKey.indexOf('=');
                 final int keyEnd = childKey.indexOf(',');
                 if (keyStart >= 0 && keyEnd >= 0 && keyStart < keyEnd) {    
                     childKey = childKey.substring(keyStart + 1, keyEnd);
                 }
```

Before the change, the childKey was just used “as-is” in the rest of the method. The above additional processing of parsing the childKey, and using a substring from it, is the code that needed to be an option that is not used by default.

In this case, the only value needed is childKey. And the only effect is for childKey to be changed. This did not take a special IDE feature to extract, but in more complex refactors, tools could come in handy.

After identifying the code to refactor, we needed to create a Java Interface with a proper name that clearly describes the strategy feature. The name we eventually landed on was IChildKeyModifier. Here is the entire interface:

```
 package org.apereo.portal.groups.smartldap;
 
  /**
  * Strategy for modifying {@LdapRecord} children keys to group keys.
  */
 public interface IChildKeyModifier {
 
      /**
      * Modify {@LdapRecord} child key to group key.
      *
      * @param ldapKey   Child key from the LDAP record
      * @return String   child key converted to group key
      */
     String convertLdapKey(String ldapKey);
 
  }
```

It’s very simple. The interface has a single method that takes a String, the child key, and returns a String to replace the current value.

Next, we simply create a Java Class that implements this interface with the code shown above:

```
package org.apereo.portal.groups.smartldap;
 
  public class ChildKeyAsCnModifier implements IChildKeyModifier {
 
      @Override
     public String convertLdapKey(String ldapKey) {
         final int keyStart = ldapKey.indexOf('=');
         final int keyEnd = ldapKey.indexOf(',');
         if (keyStart >= 0 && keyEnd >= 0 && keyStart < keyEnd) {
             ldapKey = ldapKey.substring(keyStart + 1, keyEnd);
         }
         return ldapKey;
     }
 }
```

So far, this is all straightforward. Next, we want to modify SmartLdapGroupStore to use this when configured. To do this, we can use Spring’s autowired with the optional required value set to false. This allows us to not have a bean that implements IChildKeyModifier in the Spring context.

```
     @Autowired(required = false)
     private IChildKeyModifier childKeyModifier;
```

With these two additional lines in the group store class, Spring will try to find a bean that implements this interface and set the above property to the bean. If it does not find such a bean, Spring will continue on its merry way and the property will be null.

Now, we can use the bean that implements our strategy, if it’s defined in the Spring context. Here is what replaces the code block of the original custom code:

```
                if (childKeyModifier != null) {
                 childKey = childKeyModifier.convertLdapKey(childKey);
                }
```

And that’s the end of the refactor to use Strategy pattern to customize code. If a Spring bean is declared of IChildKeyModifier, we use it. If not, we leave childKey alone.

Great! We have modified the class to use a bean, optionally. We also have such a bean. But how do we use it? It is important to understand that uPortal is now really two Git repositories: uPortal and uPortal-start. uPortal is where Java code goes. These changes go in this repo. (Default configuration would also go here.) uPortal-start would be used by a deployer to build a custom uPortal service. To implement an optional strategy bean, like ChildKeyAsCnModifier, a deployer would add a bean definition of this class in their overridesContext.xml in uPortal-start. When the uPortal web app starts, Spring will load the contexts from uPortal, then load any others defined in overridesContext.xml, from uPortal-start.

Let’s get back to how this real example is not quite up to the Strategy pattern. This might be splitting hairs, but it’s worth pointing out. If I were making this code change with the idea of using it to show others about the pattern, there would have been a default implementation, like:

```
package org.apereo.portal.groups.smartldap;
 
  public class ChildKeyNoOpModifier implements IChildKeyModifier {
 
      @Override
     public String convertLdapKey(String ldapKey) {
         return ldapKey;
     }
 }
```

If we had this class, we could make sure it was defined as a Spring bean and remove the required option along with the ‘if’ check.

The additional effort for a default implementation is important to understand as it is more common than a no-op. To replace a bean in Spring, or more to the point, to make sure there is only one bean of a particular interface, we need to use bean IDs. In our Spring context, we would have to define a well-known name for these strategies, like smartLdapChildKeyModify, for deployers to use. When a deployer wants to override such a bean with another, they must use the same ID. This can be done in their uPortal-start repo, so they do not have to customize the uPortal repo per our previous discussion.

To wrap this post up, developers can leverage Decorator and Strategy patterns to isolate customizations that can leverage configuration to drive usage. Submitting pull requests to the uPortal repo with these approaches are welcome and greatly appreciated.

But, what if there is a desire to keep the custom code local rather than have it be included in the official uPortal repo? First, I would discourage that. If the customization is in the official repo, the community will be much more likely to avoid breaking a customization. They can see the code. When code is local, developers from other institutions that lack visibility may introduce incompatible changes.

But if you really want to add custom code locally, we will cover that in our final blog post.

Stay tuned, and contact us with any questions!

Useful Reading:

Benito Gonzalez

Benito Gonzalez

Senior Software Developer
Benito Gonzalez is a Software Architect at Unicon, Inc., a leading provider of education technology consulting and digital services. Benito joined Unicon in 2015 and has over 25 years of professional IT experience. He holds a Bachelor of Science degree in Computer Science. Benito has been active in web development since 1999. Benito spent six years prior to joining Unicon as the Enterprise Web Applications Manager at University of California, Merced, where he managed several campus-wide services, such as CAS, uPortal and Sakai CLE.