How To: Add Struts Interceptors at Runtime

Version 3

    Problem

     

    Consider the scenario: custom web services in a plugin allow for the creation and management of social groups.  For these social groups created via these web services, the ability for anyone to modify the settings of the group, invite users or change the layout must be removed.

     

    Initial Thoughts

     

    The first thing that came to mind was the all powerful overlay.  It would be easy enough to overlay the actions to reject social groups that met a certain criteria.  However, this is an invasive approach requiring a custom war and significant effort to upgrade.

     

    The next thought was to redefine the appropriate actions in the struts config for the plugin.  This would have worked but not a desired approach because:

     

    • Only one plugin can redefine an action in this way.  Not that this a likely possibility, but if / when it does happen, its usually difficult to track down and not fun to resolve
    • You have to copy/paste the struts configuration from core.  This makes the customizations more brittle at upgrades because you have to check to make sure the struts configuration did not change.

     

    Solution

     

    All we really needed to do was check if the group met certain criteria before the action ran.  If so, allow the action, if not, deny.  This is the problem that struts interceptors were made to solve.  Unfortunately, there wasn't a clear cut way to add them to an existing action without redefining the struts config.  As I mentioned above, I did not want to do that.

     

    Fortunately, there is a way around this.

     

    The plugin framework takes advantage of the struts ConfigurationProvider interface which allows you to add additional configuration.  This is how plugins get their struts actions added.  Knowing that, you can write a utility class that adds another ConfigurationProvider implementation that looks up an action and adds an Interceptor to it:

     

    public class RuntimeInterceptorConfiguration implements ConfigurationProvider {
    
        private EventDispatcher eventDispatcher;
        private Map<String, Interceptor> interceptors;
        private Configuration configuration;
    
        public void setInterceptors(Map<String, Interceptor> interceptors) {
            this.interceptors = interceptors;
        }
    
        public void setEventDispatcher(EventDispatcher eventDispatcher) {
            this.eventDispatcher = eventDispatcher;
        }
    
        public void initialize() {
            eventDispatcher.dispatch(new AddConfigurationProviderEvent(this, this));
        }
    
        @Override
        public void init(Configuration configuration) throws ConfigurationException {
            this.configuration = configuration;
        }
    
        @Override
        public void loadPackages() throws ConfigurationException {
            for (String actionName : interceptors.keySet()) {
                ActionConfig actionConfig = getActionConfig(actionName);
                Interceptor interceptor = interceptors.get(actionName);
                String mappingName = interceptor.getClass().getSimpleName();
            
                actionConfig.addInterceptor(new InterceptorMapping(mappingName, interceptor));
            }
        }
    
        protected ActionConfig getActionConfig(String targetActionName) {
            for (Object packageConfigObject : configuration.getPackageConfigs().values()) {
                PackageConfig packageConfig = (PackageConfig) packageConfigObject;
                Map<String, ActionConfig> actionConfigs = packageConfig.getActionConfigs();
            
                for (String actionName : actionConfigs.keySet()) {
                    if (targetActionName.equals(actionName)) {
                        return actionConfigs.get(actionName);
                    }
                }
            }
        
            throw new IllegalArgumentException("No action with name: "+targetActionName+" could be found");
        }
    }
    
    
    

     

    The class is then used in spring:

     

    <bean class="xxx.RuntimeInterceptorConfiguration" init-method="initialize">
        <property name="eventDispatcher" ref="eventDispatcher"/>
        <property name="interceptors">
            <map>
                <entry key="edit-group">
                    <bean class="xxx.EditSocialGroupInterceptor"/>
                </entry>
            </map>
        </property>
    </bean>
    
    
    

     

    Using the above configuration will add the EditSocialGroupInterceptor to the interceptor chain only for the edit-group action.

     

    When the RuntimeConfiguration#initialize() method is called, it fires off an AddConfigurationProviderEvent.  This is a core event that notifies the struts initialization code that a new ConfigurationProvider should be processed

     

    Summary

     

    The interceptors are quite powerful, you can change what gets injected in to the actions before or after they are executed which makes it easy to modify the behavior without touching the actual code.  I feel that adding interceptors in this manner provides a clean, relatively upgrade safe way to modify the behavior of core actions without the need for an overlay or manual struts reconfiguration.

     

    Thanks to Kevin.Conaway for the great write-up!