DefaultFactoryService.java

package org.apache.fulcrum.factory;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.avalon.framework.activity.Disposable;
import org.apache.avalon.framework.activity.Initializable;
import org.apache.avalon.framework.configuration.Configurable;
import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.ConfigurationException;
import org.apache.avalon.framework.logger.AbstractLogEnabled;
import org.apache.fulcrum.factory.utils.ObjectInputStreamForContext;

/**
 * The Factory Service instantiates objects using specified class loaders. If
 * none is specified, the default one will be used.
 * 
 * avalon.component name="factory" lifestyle="singleton" avalon.service
 * type="org.apache.fulcrum.factory.FactoryService"
 *
 * @author <a href="mailto:epugh@upstate.com">Eric Pugh</a>
 * @author <a href="mailto:ilkka.priha@simsoft.fi">Ilkka Priha</a>
 * @author <a href="mailto:mcconnell@apache.org">Stephen McConnell</a>
 * @version $Id$
 *
 */
public class DefaultFactoryService extends AbstractLogEnabled
		implements FactoryService, Configurable, Initializable, Disposable {

	/**
	 * The property specifying a set of additional class loaders.
	 */
	private static final String CLASS_LOADER = "classloader";

	/**
	 * The property prefix specifying additional object factories.
	 */
	private static final String OBJECT_FACTORY = "object-factory";

	/**
	 * The name of the default factory.
	 */
	protected static final String DEFAULT_FACTORY = "default";

	/**
	 * Primitive classes for reflection of constructors.
	 */
	private static HashMap<String, Class<?>> primitiveClasses = new HashMap<String, Class<?>>(8);

	{
		primitiveClasses.put(Boolean.TYPE.toString(), Boolean.TYPE);
		primitiveClasses.put(Character.TYPE.toString(), Character.TYPE);
		primitiveClasses.put(Byte.TYPE.toString(), Byte.TYPE);
		primitiveClasses.put(Short.TYPE.toString(), Short.TYPE);
		primitiveClasses.put(Integer.TYPE.toString(), Integer.TYPE);
		primitiveClasses.put(Long.TYPE.toString(), Long.TYPE);
		primitiveClasses.put(Float.TYPE.toString(), Float.TYPE);
		primitiveClasses.put(Double.TYPE.toString(), Double.TYPE);
	}

	/**
	 * temporary storage of class names between configure and initialize
	 */
	private String[] loaderNames;
	/**
	 * Additional class loaders.
	 */
	private ArrayList<ClassLoader> classLoaders = new ArrayList<ClassLoader>();
	/**
	 * Customized object factories.
	 */
	private ConcurrentHashMap<String, Factory<?>> objectFactories = new ConcurrentHashMap<String, Factory<?>>();
	/**
	 * Customized object factory classes.
	 */
	private ConcurrentHashMap<String, String> objectFactoryClasses = new ConcurrentHashMap<String, String>();

	/**
	 * Gets the class of a primitive type.
	 *
	 * @param type a primitive type.
	 * @return the corresponding class, or null.
	 */
	protected static Class<?> getPrimitiveClass(String type) 
	{
		return primitiveClasses.get(type);
	}

	/**
	 * Gets an instance of a named class.
	 *
	 * @param className the name of the class.
	 * @return the instance.
	 * @throws FactoryException if instantiation fails.
	 */
	@Override
	public <T> T getInstance(String className) throws FactoryException 
	{
		if (className == null) {
			throw new FactoryException("Missing String className");
		}
		Factory<T> factory = getFactory(className);
		if (factory == null) {
			Class<T> clazz;
			try {
				clazz = loadClass(className);
			} catch (ClassNotFoundException x) {
				throw new FactoryException("Instantiation failed for class " + className, x);
			}
			return getInstance(clazz);
		} else {
			return factory.getInstance();
		}
	}

	/**
	 * Gets an instance of a named class using a specified class loader.
	 *
	 * <p>
	 * Class loaders are supported only if the isLoaderSupported method returns
	 * true. Otherwise the loader parameter is ignored.
	 *
	 * @param className the name of the class.
	 * @param loader    the class loader.
	 * @return the instance.
	 * @throws FactoryException if instantiation fails.
	 */
	@Override
	public <T> T getInstance(String className, ClassLoader loader) throws FactoryException 
	{
		Factory<T> factory = getFactory(className);
		if (factory == null) {
			if (loader != null) {
				Class<T> clazz;
				try {
					clazz = loadClass(className, loader);
				} catch (ClassNotFoundException x) {
					throw new FactoryException("Instantiation failed for class " + className, x);
				}
				return getInstance(clazz);
			} else {
				return getInstance(className);
			}
		} else {
			return factory.getInstance(loader);
		}
	}

	/**
	 * Gets an instance of a named class. Parameters for its constructor are given
	 * as an array of objects, primitive types must be wrapped with a corresponding
	 * class.
	 *
	 * @param className the name of the class.
	 * @param params    an array containing the parameters of the constructor.
	 * @param signature an array containing the signature of the constructor.
	 * @return the instance.
	 * @throws FactoryException if instantiation fails.
	 */
	@Override
	public <T> T getInstance(String className, Object[] params, String[] signature) throws FactoryException 
	{
		Factory<T> factory = getFactory(className);
		if (factory == null) {
			Class<T> clazz;
			try {
				clazz = loadClass(className);
			} catch (ClassNotFoundException x) {
				throw new FactoryException("Instantiation failed for class " + className, x);
			}
			return getInstance(clazz, params, signature);
		} else {
			return factory.getInstance(params, signature);
		}
	}

	/**
	 * Gets an instance of a named class using a specified class loader. Parameters
	 * for its constructor are given as an array of objects, primitive types must be
	 * wrapped with a corresponding class.
	 *
	 * <p>
	 * Class loaders are supported only if the isLoaderSupported method returns
	 * true. Otherwise the loader parameter is ignored.
	 * </p>
	 *
	 * @param           <T> Type of the class
	 * @param className the name of the class.
	 * @param loader    the class loader.
	 * @param params    an array containing the parameters of the constructor.
	 * @param signature an array containing the signature of the constructor.
	 * @return the instance.
	 * @throws FactoryException if instantiation fails.
	 */
	@Override
	public <T> T getInstance(String className, ClassLoader loader, Object[] params, String[] signature)
			throws FactoryException 
	{
		Factory<T> factory = getFactory(className);
		if (factory == null) {
			if (loader != null) {
				Class<T> clazz;
				try {
					clazz = loadClass(className, loader);
				} catch (ClassNotFoundException x) {
					throw new FactoryException("Instantiation failed for class " + className, x);
				}
				return getInstance(clazz, params, signature);
			} else {
				return getInstance(className, params, signature);
			}
		} else {
			return factory.getInstance(loader, params, signature);
		}
	}

	/**
	 * Tests if specified class loaders are supported for a named class.
	 *
	 * @param className the name of the class.
	 * @return true if class loaders are supported, false otherwise.
	 * @throws FactoryException if test fails.
	 */
	@Override
	public boolean isLoaderSupported(String className) throws FactoryException 
	{
		Factory<?> factory = getFactory(className);
		return factory != null ? factory.isLoaderSupported() : true;
	}

	/**
	 * Gets an instance of a specified class.
	 *
	 * @param           <T> Type of the class
	 * @param clazz the class.
	 * @return the instance.
	 * @throws FactoryException if instantiation fails.
	 */
	@Override
	public <T> T getInstance(Class<T> clazz) throws FactoryException 
	{
		try {
			return clazz.newInstance();
		} catch (Exception x) {
			throw new FactoryException("Instantiation failed for " + clazz.getName(), x);
		}
	}

	/**
	 * Gets an instance of a specified class. Parameters for its constructor are
	 * given as an array of objects, primitive types must be wrapped with a
	 * corresponding class.
	 *
	 * @param           <T> Type of the class
	 * @param clazz     the class
	 * @param params    an array containing the parameters of the constructor
	 * @param signature an array containing the signature of the constructor
	 * @return the instance
	 * @throws FactoryException if instantiation fails.
	 */
	protected <T> T getInstance(Class<T> clazz, Object params[], String signature[]) 
			throws FactoryException 
	{
		/* Try to construct. */
		try {
			Class<?>[] sign = getSignature(clazz, params, signature);
			return clazz.getConstructor(sign).newInstance(params);
		} catch (Exception x) {
			throw new FactoryException("Instantiation failed for " + clazz.getName(), x);
		}
	}

	/**
	 * Gets the signature classes for parameters of a method of a class.
	 *
	 * @param clazz     the class.
	 * @param params    an array containing the parameters of the method.
	 * @param signature an array containing the signature of the method.
	 * @return an array of signature classes. Note that in some cases objects in the
	 *         parameter array can be switched to the context of a different class
	 *         loader.
	 * @throws ClassNotFoundException if any of the classes is not found.
	 */
	@Override
	public Class<?>[] getSignature(Class<?> clazz, Object params[], String signature[]) 
			throws ClassNotFoundException 
	{
		if (signature != null) {
			/* We have parameters. */
			ClassLoader tempLoader;
			ClassLoader loader = clazz.getClassLoader();
			Class<?>[] sign = new Class[signature.length];
			for (int i = 0; i < signature.length; i++) {
				/* Check primitive types. */
				sign[i] = getPrimitiveClass(signature[i]);
				if (sign[i] == null) {
					/* Not a primitive one, continue building. */
					if (loader != null) {
						/* Use the class loader of the target object. */
						sign[i] = loader.loadClass(signature[i]);
						tempLoader = sign[i].getClassLoader();
						if (params[i] != null && tempLoader != null
								&& !tempLoader.equals(params[i].getClass().getClassLoader())) {
							/*
							 * The class uses a different class loader, switch the parameter.
							 */
							params[i] = switchObjectContext(params[i], loader);
						}
					} else {
						/* Use the default class loader. */
						sign[i] = loadClass(signature[i]);
					}
				}
			}
			return sign;
		} else {
			return null;
		}
	}

	/**
	 * Switches an object into the context of a different class loader.
	 *
	 * @param object an object to switch.
	 * @param loader the ClassLoader to use
	 * @param loader the loader of the new context.
	 * @return the object
	 */
	protected Object switchObjectContext(Object object, ClassLoader loader) 
	{
		ByteArrayOutputStream bout = new ByteArrayOutputStream();

		try 
		{
			ObjectOutputStream out = new ObjectOutputStream(bout);
			out.writeObject(object);
			out.flush();
		} 
		catch (IOException x) 
		{
			return object;
		}

		ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
		ObjectInputStreamForContext in = null;

		try 
		{
			in = new ObjectInputStreamForContext(bin, loader);
			return in.readObject();
		} 
		catch (Exception x) 
		{
			return object;
		} 
		finally 
		{
			if (in != null) 
			{
				try 
				{
					in.close();
				} 
				catch (IOException e) 
				{
					// close quietly
				}
			}
		}
	}

	/**
	 * Loads the named class using the default class loader.
	 *
	 * @param className the name of the class to load.
	 * @return {@inheritDoc} the loaded class.
	 * @throws ClassNotFoundException if the class was not found.
	 */
	@SuppressWarnings("unchecked")
	protected <T> Class<T> loadClass(String className) throws ClassNotFoundException 
	{
		ClassLoader loader = this.getClass().getClassLoader();
		try 
		{
			Class<T> clazz;

			if (loader != null) 
			{
				clazz = (Class<T>) loader.loadClass(className);
			} 
			else 
			{
				clazz = (Class<T>) Class.forName(className);
			}

			return clazz;
		} 
		catch (ClassNotFoundException x) 
		{
			/* Go through additional loaders. */
			for (ClassLoader l : classLoaders) 
			{
				try 
				{
					return (Class<T>) l.loadClass(className);
				} 
				catch (ClassNotFoundException xx) 
				{
					// continue
				}
			}
			/* Give up. */
			throw x;
		}
	}

	/**
	 * Loads the named class using a specified class loader.
	 *
	 * @param className the name of the class to load.
	 * @param loader    the loader to use.
	 * @return {@inheritDoc} the loaded class.
	 * @throws ClassNotFoundException if the class was not found.
	 */
	@SuppressWarnings("unchecked")
	protected <T> Class<T> loadClass(String className, ClassLoader loader) throws ClassNotFoundException 
	{
		if (loader != null) 
		{
			return (Class<T>) loader.loadClass(className);
		} 
		else 
		{
			return loadClass(className);
		}
	}

	/**
	 * Gets a customized factory for a named class. If no class-specific factory is
	 * specified but a default factory is, will use the default factory.
	 *
	 * @param className the name of the class to load.
	 * @return {@inheritDoc} the factory, or null if not specified and no default.
	 * @throws FactoryException if instantiation of the factory fails.
	 */
	@SuppressWarnings("unchecked")
	protected <T> Factory<T> getFactory(String className) throws FactoryException 
	{
		Factory<T> factory = (Factory<T>) objectFactories.get(className);
		if (factory == null) 
		{
			// No named factory for this; try the default, if one exists
			factory = (Factory<T>) objectFactories.get(DEFAULT_FACTORY);
		}
		
		if (factory == null) {
			
			/* Not yet instantiated... */
			String factoryClass = objectFactoryClasses.get(className);
			if (factoryClass == null) 
			{
				factoryClass = objectFactoryClasses.get(DEFAULT_FACTORY);
			}
			
			if (factoryClass == null) {
				return null;
			}

			try {
				factory = getInstance(factoryClass);
				factory.init(className);
			} 
			catch (ClassCastException x) 
			{
				throw new FactoryException("Incorrect factory " + factoryClass + " for class " + className, x);
			}
			
			Factory<T> _factory = (Factory<T>) objectFactories.putIfAbsent(className, factory);
			if (_factory != null) 
			{
				// Already created - take first instance
				factory = _factory;
			}
		}

		return factory;
	}

	// ---------------- Avalon Lifecycle Methods ---------------------

	/* (non-Javadoc)
	 * Avalon component lifecycle method
	 * @see org.apache.avalon.framework.configuration.Configurable#configure(org.apache.avalon.framework.configuration.Configuration)
	 */
	@Override
	public void configure(Configuration conf) throws ConfigurationException 
	{
		final Configuration[] loaders = conf.getChildren(CLASS_LOADER);
		if (loaders != null) 
		{
			loaderNames = new String[loaders.length];
			for (int i = 0; i < loaders.length; i++) 
			{
				loaderNames[i] = loaders[i].getValue();
			}
		}

		final Configuration factories = conf.getChild(OBJECT_FACTORY, false);
		if (factories != null) 
		{
			// Store the factory to the table as a string and
			// instantiate it by using the service when needed.
			Configuration[] nameVal = factories.getChildren();
			for (Configuration entry : nameVal)
				objectFactoryClasses.put(entry.getName(), entry.getValue());

		}
	}

	/**
	 * Avalon component lifecycle method Initializes the service by loading default
	 * class loaders and customized object factories.
	 *
	 * @throws Exception if initialization fails.
	 */
	@Override
	public void initialize() throws Exception 
	{
		if (loaderNames != null) 
		{
			for (String className : loaderNames) 
			{
				try 
				{
					ClassLoader loader = (ClassLoader) loadClass(className).newInstance();
					classLoaders.add(loader);
				} 
				catch (Exception x) 
				{
					throw new Exception("No such class loader '" + className + "' for DefaultFactoryService", x);
				}
			}
			loaderNames = null;
		}
	}

	/**
	 * Avalon component lifecycle method Clear lists and maps
	 */
	@Override
	public void dispose() 
	{
		objectFactories.clear();
		objectFactoryClasses.clear();
		classLoaders.clear();
	}
}