Dashboard > RIFE > ... > Tips and Tricks > IoC support for non-element objects
RIFE Log In | Sign Up   View a printable version of the current page.
IoC support for non-element objects


Added by Steven Grimm, last edited by Steven Grimm on Aug 07, 2006
Labels: 
(None)

RIFE's built-in IoC only supports injecting dependencies into element objects. But it is often beneficial to inject dependencies into other objects. For example, a Datasource object might be needed by a class that implements business logic for your site.

The most flexible way to do this is to use a full-fledged IoC container like Spring. But Spring is more heavyweight than is sometimes desirable; another approach is to use a participant that does the appropriate dependency injection.

DependencyParticipant.java
import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Map.Entry;

import com.uwyn.rife.ioc.HierarchicalProperties;
import com.uwyn.rife.rep.BlockingParticipant;
import com.uwyn.rife.tools.BeanPropertyProcessor;
import com.uwyn.rife.tools.BeanUtils;
import com.uwyn.rife.tools.exceptions.BeanUtilsException;

/**
 * Participant that instantiates and initializes a list of arbitrary objects,
 * adding them to the RIFE global properties list so they will be injected
 * into elements.
 * <p>
 * For each setter in an object, if there is already a compatible object of
 * the same name in the global properties list, it will be passed to the setter.
 * This dependency injection happens after the objects are instantiated, so they
 * may freely refer to one another.
 * <p>
 * This is a useful lightweight replacement for the dependency injection
 * in something like Spring, when the full power of Spring isn't required.
 * <p>
 * The parameter is a properties file with a list of parameter names and
 * classes. The class will be instantiated and listed under the name in the
 * global properties. For example:
 * <pre>accountDao=com.foo.dao.AccountDaoImpl
 * accountManager=com.foo.AccountManager
 * preferencesDao=com.foo.dao.PreferencesDaoImpl</pre>
 * <p>
 * This will instantiate <code>com.foo.dao.AccountDaoImpl</code> and add
 * it to the global properties as "accountDao". If the <code>AccountManager</code>
 * class has a <code>setAccountDao</code> method that can take an
 * <code>AccountDaoImpl</code> parameter, the setter will be called.
 */
public class DependencyParticipant extends BlockingParticipant {
    private Map<String, Object> _beans;
    private HierarchicalProperties _globalProperties;

    /**
     * Instantiates a bean and adds it to the bean map.
     */
    private void createBean(String id, String className) {
	try {
	    Class c = Class.forName(className);
	    Object o = c.newInstance();
	    
	    _beans.put(id, o);
	}
	catch (ClassNotFoundException e) {
	    throw new DependencyParticipantException("Class " + className + 
		    " not found", e);
	}
	catch (InstantiationException e) {
	    throw new DependencyParticipantException("Class " + className +
		    " can't be instantiated", e);
	}
	catch (IllegalAccessException e) {
	    throw new DependencyParticipantException("Class " + className +
		    " has permissions that don't allow access", e);
	}
    }

    /**
     * Injects values into an object.
     */
    private void injectObject(final Object o) {
	try {
	    BeanUtils.processProperties(BeanUtils.SETTERS, o.getClass(), null, null,
					null, new BeanPropertyProcessor() {
        	    public void gotProperty(String name, PropertyDescriptor descriptor)
        	    		throws IllegalAccessException, IllegalArgumentException, 
        	    				InvocationTargetException {
        		if (_globalProperties.contains(name)) {
        		    Object candidate = _globalProperties.getValue(name);
        		    Method m = descriptor.getWriteMethod();
        		    Class[] paramTypes = m.getParameterTypes();
        		    if (paramTypes.length == 1 && paramTypes[0].isAssignableFrom(candidate.getClass())) {
        			m.invoke(o, new Object[] { candidate });
        		    }
        		}
        	    }
        	});
	}
	catch (BeanUtilsException e) {
	    throw new DependencyParticipantException("Bean " +
		    o.getClass().getName() + " can't be injected", e);
	}
    }
    
    @Override
    protected void initialize() {
	InputStream is = getClass().getResourceAsStream("/" + getParameter());
	if (is == null)
	    throw new DependencyParticipantException("Bean list not found: " +
		    getParameter());
	
	_globalProperties = getRepository().getProperties();
	Properties props = new Properties();

	try {
	    props.load(is);
	    is.close();
	}
	catch (IOException e) {
	    throw new DependencyParticipantException("Bean list can't be read",e);
	}

	_beans = new HashMap<String, Object>();

        /* First instantiate all the objects. */
        for (Entry e : props.entrySet()) {
            createBean((String) e.getKey(), (String) e.getValue());
        }
    
        /* Now add them all to the global properties. */
        getRepository().getProperties().putAll(_beans);

        /* And finally, inject them (and other properties) into each other. */
        for (Object o : _beans.values()) {
            injectObject(o);
        }
        
        _beans = null;
    }
}
DependencyParticipantException.java
/**
 * Exception thrown by the dependency participant when it has trouble
 * setting up the dependencies.
 */
public class DependencyParticipantException extends RuntimeException {
    public DependencyParticipantException(String s) {
	super(s);
    }
    
    public DependencyParticipantException(String s, Throwable t) {
	super(s, t);
    }
}

This class implements a new participant, which you can include in your participants file like so:

<participant param="rep/beans.properties" blocking="true">
    	com.foo.DependencyParticipant
</participant>

The class reads the properties file named by its parameter. This is a simple map of property names to class names:

userManager=com.foo.service.UserManager
userDao=com.foo.db.UserDaoImpl

It adds an instance of each of the named classes to the RIFE global properties list. Then it walks through each instance and, for each setter method where there is a matching entry in the global properties list, passes that entry's value to the setter.

The effect is that all the beans named in the list are (to use Spring's terminology) autowired by name to one another, and any global properties defined earlier in the participant configuration (e.g. a "datasource" property) are wired into the beans by name as well. Note that the property name is used, not the name of the class; in the example above, the UserDaoImpl bean will be passed to other beans' setUserDao() methods.

So, for example:

UserManager.java
package com.foo.service;

import com.foo.db.UserDao;

public class UserManager() {
    private UserDao userDao;
    public void setUserDao(UserDaoImpl dao) {
        this.userDao = dao;
    }
}

Any objects created by this participant will automatically be wired into element classes, of course.



Are you enjoying Confluence? Please consider purchasing it today.
Powered by Atlassian Confluence, the Enterprise Wiki. (Version: 2.2.1a Build:#515 May 19, 2006) - Bug/feature request - Contact Administrators