/*******************************************************************************
 * Copyright (c) 2003, 2013 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/

package org.eclipse.osgi.internal.serviceregistry;

import java.security.AccessController;
import java.security.PrivilegedAction;
import org.eclipse.osgi.internal.debug.Debug;
import org.eclipse.osgi.internal.framework.BundleContextImpl;
import org.eclipse.osgi.internal.messages.Msg;
import org.eclipse.osgi.util.NLS;
import org.osgi.framework.*;

/**
 * This class represents the use of a service by a bundle. One is created for each
 * service acquired by a bundle.
 *
 * <p>
 * This class manages a service factory.
 *
 * @ThreadSafe
 */
public class ServiceFactoryUse<S> extends ServiceUse<S> {
	/** BundleContext associated with this service use */
	final BundleContextImpl context;
	/** ServiceFactory object  */
	final ServiceFactory<S> factory;
	final Debug debug;

	/** Service object returned by ServiceFactory.getService() */
	/* @GuardedBy("this") */
	private S cachedService;
	/** true if we are calling the factory getService method. Used to detect recursion. */
	/* @GuardedBy("this") */
	private boolean factoryInUse;

	/**
	 * Constructs a service use encapsulating the service factory.
	 *
	 * @param   context bundle getting the service
	 * @param   registration ServiceRegistration of the service
	 */
	ServiceFactoryUse(BundleContextImpl context, ServiceRegistrationImpl<S> registration) {
		super(context, registration);
		this.debug = context.getContainer().getConfiguration().getDebug();
		this.context = context;
		this.factoryInUse = false;
		this.cachedService = null;
		@SuppressWarnings("unchecked")
		ServiceFactory<S> f = (ServiceFactory<S>) registration.getServiceObject();
		this.factory = f;
	}

	/**
	 * Get a service's service object and increment the use count.
	 *
	 * <p>The following steps are followed to get the service object:
	 * <ol>
	 * <li>The use count is incremented by one.
	 * <li>If the use count is now one,
	 * the {@link ServiceFactory#getService(Bundle, ServiceRegistration)} method
	 * is called to create a service object for the context bundle.
	 * This service object is cached.
	 * While the use count is greater than zero,
	 * subsequent calls to get the service object
	 * will return the cached service object.
	 * <br>If the service object returned by the {@link ServiceFactory}
	 * is not an <code>instanceof</code>
	 * all the classes named when the service was registered or
	 * the {@link ServiceFactory} throws an exception,
	 * <code>null</code> is returned and a
	 * {@link FrameworkEvent} of type {@link FrameworkEvent#ERROR} is broadcast.
	 * <li>The service object is returned.
	 * </ol>
	 *
	 * @return The service object.
	 */
	/* @GuardedBy("this") */
	@Override
	S getService() {
		assert Thread.holdsLock(this);
		if (inUse()) {
			incrementUse();
			return cachedService;
		}

		if (debug.DEBUG_SERVICES) {
			Debug.println("getService[factory=" + registration.getBundle() + "](" + context.getBundleImpl() + "," + registration + ")"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
		}
		// check for recursive call on this thread
		if (factoryInUse) {
			if (debug.DEBUG_SERVICES) {
				Debug.println(factory + ".getService() recursively called."); //$NON-NLS-1$
			}

			ServiceException se = new ServiceException(NLS.bind(Msg.SERVICE_FACTORY_RECURSION, factory.getClass().getName(), "getService"), ServiceException.FACTORY_RECURSION); //$NON-NLS-1$
			context.getContainer().getEventPublisher().publishFrameworkEvent(FrameworkEvent.WARNING, registration.getBundle(), se);
			return null;
		}
		factoryInUse = true;
		final S service;
		try {
			service = factoryGetService();
			if (service == null) {
				return null;
			}
		} finally {
			factoryInUse = false;
		}

		this.cachedService = service;
		incrementUse();

		return service;
	}

	/**
	 * Unget a service's service object.
	 *
	 * <p>
	 * Decrements the use count if the service was being used.
	 *
	 * <p>The following steps are followed to unget the service object:
	 * <ol>
	 * <li>If the use count is zero, return.
	 * <li>The use count is decremented by one.
	 * <li>If the use count is non zero, return.
	 * <li>The {@link ServiceFactory#ungetService(Bundle, ServiceRegistration, Object)} method
	 * is called to release the service object for the context bundle.
	 * </ol>
	 * @return true if the service was ungotten; otherwise false.
	 */
	/* @GuardedBy("this") */
	@Override
	boolean ungetService() {
		assert Thread.holdsLock(this);
		if (!inUse()) {
			return false;
		}

		decrementUse();
		if (inUse()) {
			return true;
		}

		final S service = cachedService;
		cachedService = null;

		if (debug.DEBUG_SERVICES) {
			Debug.println("ungetService[factory=" + registration.getBundle() + "](" + context.getBundleImpl() + "," + registration + ")"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
		}
		factoryUngetService(service);
		return true;
	}

	/**
	 * Release all uses of the service and reset the use count to zero.
	 *
	 * <ol>
	 * <li>The bundle's use count for this service is set to zero.
	 * <li>The {@link ServiceFactory#ungetService(Bundle, ServiceRegistration, Object)} method
	 * is called to release the service object for the bundle.
	 * </ol>
	 */
	/* @GuardedBy("this") */
	@Override
	void release() {
		super.release();

		final S service = cachedService;
		if (service == null) {
			return;
		}
		cachedService = null;

		if (debug.DEBUG_SERVICES) {
			Debug.println("releaseService[factory=" + registration.getBundle() + "](" + context.getBundleImpl() + "," + registration + ")"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
		}
		factoryUngetService(service);
	}

	/**
	 * Return the service object for this service use.
	 *
	 * @return The service object.
	 */
	/* @GuardedBy("this") */
	@Override
	S getCachedService() {
		return cachedService;
	}

	/**
	 *  Call the service factory to get the service.
	 *
	 * @return The service returned by the factory or null if there was an error.
	 */
	/* @GuardedBy("this") */
	S factoryGetService() {
		final S service;
		try {
			service = AccessController.doPrivileged(new PrivilegedAction<S>() {
				@Override
				public S run() {
					return factory.getService(context.getBundleImpl(), registration);
				}
			});
		} catch (Throwable t) {
			if (debug.DEBUG_SERVICES) {
				Debug.println(factory + ".getService() exception: " + t.getMessage()); //$NON-NLS-1$
				Debug.printStackTrace(t);
			}
			// allow the adaptor to handle this unexpected error
			context.getContainer().handleRuntimeError(t);
			ServiceException se = new ServiceException(NLS.bind(Msg.SERVICE_FACTORY_EXCEPTION, factory.getClass().getName(), "getService"), ServiceException.FACTORY_EXCEPTION, t); //$NON-NLS-1$
			context.getContainer().getEventPublisher().publishFrameworkEvent(FrameworkEvent.ERROR, registration.getBundle(), se);
			return null;
		}

		if (service == null) {
			if (debug.DEBUG_SERVICES) {
				Debug.println(factory + ".getService() returned null."); //$NON-NLS-1$
			}

			ServiceException se = new ServiceException(NLS.bind(Msg.SERVICE_OBJECT_NULL_EXCEPTION, factory.getClass().getName()), ServiceException.FACTORY_ERROR);
			context.getContainer().getEventPublisher().publishFrameworkEvent(FrameworkEvent.WARNING, registration.getBundle(), se);
			return null;
		}

		String[] clazzes = registration.getClasses();
		String invalidService = ServiceRegistry.checkServiceClass(clazzes, service);
		if (invalidService != null) {
			if (debug.DEBUG_SERVICES) {
				Debug.println("Service object is not an instanceof " + invalidService); //$NON-NLS-1$
			}
			ServiceException se = new ServiceException(NLS.bind(Msg.SERVICE_FACTORY_NOT_INSTANCEOF_CLASS_EXCEPTION, factory.getClass().getName(), invalidService), ServiceException.FACTORY_ERROR);
			context.getContainer().getEventPublisher().publishFrameworkEvent(FrameworkEvent.ERROR, registration.getBundle(), se);
			return null;
		}
		return service;
	}

	/**
	 *  Call the service factory to unget the service.
	 *
	 *  @param service The service object to pass to the factory.
	 */
	/* @GuardedBy("this") */
	void factoryUngetService(final S service) {
		try {
			AccessController.doPrivileged(new PrivilegedAction<Void>() {
				@Override
				public Void run() {
					factory.ungetService(context.getBundleImpl(), registration, service);
					return null;
				}
			});
		} catch (Throwable t) {
			if (debug.DEBUG_SERVICES) {
				Debug.println(factory + ".ungetService() exception"); //$NON-NLS-1$
				Debug.printStackTrace(t);
			}

			ServiceException se = new ServiceException(NLS.bind(Msg.SERVICE_FACTORY_EXCEPTION, factory.getClass().getName(), "ungetService"), ServiceException.FACTORY_EXCEPTION, t); //$NON-NLS-1$
			context.getContainer().getEventPublisher().publishFrameworkEvent(FrameworkEvent.ERROR, registration.getBundle(), se);
		}
	}
}
