/******************************************************************************
 * Copyright (c) 2006, 2010 VMware Inc.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * and Apache License v2.0 which accompanies this distribution. 
 * The Eclipse Public License is available at 
 * http://www.eclipse.org/legal/epl-v10.html and the Apache License v2.0
 * is available at http://www.opensource.org/licenses/apache2.0.php.
 * You may elect to redistribute this code under either of these licenses. 
 * 
 * Contributors:
 *   VMware Inc.
 *****************************************************************************/

package org.eclipse.gemini.blueprint.config.internal.adapter;

import java.lang.reflect.Method;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.gemini.blueprint.context.support.internal.security.SecurityUtils;
import org.eclipse.gemini.blueprint.service.exporter.OsgiServiceRegistrationListener;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
 * Adapter/wrapper class that handles listener with custom method invocation. Similar in functionality to
 * {@link org.eclipse.gemini.blueprint.config.internal.adapter.OsgiServiceLifecycleListenerAdapter}.
 * 
 * @author Costin Leau
 * 
 */
public class OsgiServiceRegistrationListenerAdapter implements OsgiServiceRegistrationListener, InitializingBean,
		BeanFactoryAware {

	private static final Log log = LogFactory.getLog(OsgiServiceRegistrationListenerAdapter.class);

	/** does the target implement the listener interface */
	private boolean isListener;

	private String registrationMethod, unregistrationMethod;

	/** actual target */
	private Object target;

	/** target bean name (when dealing with cycles) */
	private String targetBeanName;

	/** bean factory used for retrieving the target when dealing with cycles */
	private BeanFactory beanFactory;

	/** init flag */
	private boolean initialized;

	/**
	 * Map of methods keyed by the first parameter which indicates the service type expected.
	 */
	private Map<Class<?>, List<Method>> registrationMethods, unregistrationMethods;

	private boolean isBlueprintCompliant = false;

	public void afterPropertiesSet() {
		Assert.notNull(beanFactory);
		Assert.isTrue(target != null || StringUtils.hasText(targetBeanName),
				"one of 'target' or 'targetBeanName' properties has to be set");

		if (target != null)
			initialized = true;

		// do validation (on the target type)
		initialize();
		// postpone target initialization until one of bind/unbind method is called
	}

	private void retrieveTarget() {
		target = beanFactory.getBean(targetBeanName);
		initialized = true;
	}

	/**
	 * Initialise adapter. Determine custom methods and do validation.
	 */
	private void initialize() {
		Class<?> clazz = (target == null ? beanFactory.getType(targetBeanName) : target.getClass());

		isListener = OsgiServiceRegistrationListener.class.isAssignableFrom(clazz);
		if (isListener)
			if (log.isDebugEnabled())
				log.debug(clazz.getName() + " is a registration listener");

		registrationMethods =
				CustomListenerAdapterUtils.determineCustomMethods(clazz, registrationMethod, isBlueprintCompliant);
		unregistrationMethods =
				CustomListenerAdapterUtils.determineCustomMethods(clazz, unregistrationMethod, isBlueprintCompliant);

		if (!isListener && (registrationMethods.isEmpty() && unregistrationMethods.isEmpty()))
			throw new IllegalArgumentException("Target object needs to implement "
					+ OsgiServiceRegistrationListener.class.getName()
					+ " or custom registered/unregistered methods have to be specified");

		if (log.isTraceEnabled()) {
			StringBuilder builder = new StringBuilder();
			builder.append("Discovered bind methods=");
			builder.append(registrationMethods.values());
			builder.append("\nunbind methods=");
			builder.append(unregistrationMethods.values());
			log.trace(builder.toString());
		}
	}

	public void registered(final Object service, final Map serviceProperties) {
		boolean trace = log.isTraceEnabled();

		if (trace)
			log.trace("Invoking registered method with props=" + serviceProperties);

		if (!initialized)
			retrieveTarget();

		boolean isSecurityEnabled = System.getSecurityManager() != null;
		AccessControlContext acc = null;
		if (isSecurityEnabled) {
			acc = SecurityUtils.getAccFrom(beanFactory);
		}

		// first call interface method (if it exists)
		if (isListener) {
			if (trace)
				log.trace("Invoking listener interface methods");

			try {
				if (isSecurityEnabled) {
					AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
						public Object run() throws Exception {
							((OsgiServiceRegistrationListener) target).registered(service, serviceProperties);
							return null;
						}
					}, acc);
				} else {
					((OsgiServiceRegistrationListener) target).registered(service, serviceProperties);
				}
			} catch (Exception ex) {
				if (ex instanceof PrivilegedActionException) {
					ex = ((PrivilegedActionException) ex).getException();
				}
				log.warn("Standard registered method on [" + target.getClass().getName() + "] threw exception", ex);
			}
		}

		if (isSecurityEnabled) {
			AccessController.doPrivileged(new PrivilegedAction<Object>() {
				public Object run() {
					CustomListenerAdapterUtils.invokeCustomMethods(target, registrationMethods, service,
							serviceProperties);
					return null;
				}
			}, acc);
		} else {
			CustomListenerAdapterUtils.invokeCustomMethods(target, registrationMethods, service, serviceProperties);
		}
	}

	public void unregistered(final Object service, final Map serviceProperties) {

		boolean trace = log.isTraceEnabled();

		if (trace)
			log.trace("Invoking unregistered method with props=" + serviceProperties);

		if (!initialized)
			retrieveTarget();

		boolean isSecurityEnabled = System.getSecurityManager() != null;
		AccessControlContext acc = null;

		if (isSecurityEnabled) {
			acc = SecurityUtils.getAccFrom(beanFactory);
		}

		// first call interface method (if it exists)
		if (isListener) {
			if (trace)
				log.trace("Invoking listener interface methods");

			try {
				if (isSecurityEnabled) {
					AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
						public Object run() throws Exception {
							((OsgiServiceRegistrationListener) target).unregistered(service, serviceProperties);
							return null;
						}
					}, acc);
				} else {
					((OsgiServiceRegistrationListener) target).unregistered(service, serviceProperties);
				}
			} catch (Exception ex) {
				log.warn("Standard unregistered method on [" + target.getClass().getName() + "] threw exception", ex);
			}
		}

		if (isSecurityEnabled) {
			AccessController.doPrivileged(new PrivilegedAction<Object>() {
				public Object run() {
					CustomListenerAdapterUtils.invokeCustomMethods(target, unregistrationMethods, service,
							serviceProperties);
					return null;
				}
			}, acc);
		} else {
			CustomListenerAdapterUtils.invokeCustomMethods(target, unregistrationMethods, service, serviceProperties);
		}
	}

	/**
	 * @param registrationMethod The registrationMethod to set.
	 */
	public void setRegistrationMethod(String registrationMethod) {
		this.registrationMethod = registrationMethod;
	}

	/**
	 * @param unregistrationMethod The unregistrationMethod to set.
	 */
	public void setUnregistrationMethod(String unregistrationMethod) {
		this.unregistrationMethod = unregistrationMethod;
	}

	public void setBeanFactory(BeanFactory beanFactory) {
		this.beanFactory = beanFactory;
	}

	/**
	 * @param target The target to set.
	 */
	public void setTarget(Object target) {
		this.target = target;
	}

	/**
	 * @param targetBeanName The targetBeanName to set.
	 */
	public void setTargetBeanName(String targetBeanName) {
		this.targetBeanName = targetBeanName;
	}

	public void setBlueprintCompliant(boolean compliant) {
		this.isBlueprintCompliant = compliant;
	}
}