/*******************************************************************************
 * Copyright (c) 2007, 2018 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.equinox.internal.event;

import java.security.*;
import java.util.Collection;
import org.eclipse.osgi.util.NLS;
import org.osgi.framework.*;
import org.osgi.service.event.*;
import org.osgi.service.log.LogService;

/**
 * A wrapper for EventHandlers. This class caches property values and 
 * performs final checks before calling the wrapped handler.
 *
 */
public class EventHandlerWrapper {
	final ServiceReference<EventHandler> reference;
	private final LogTracker log;
	final BundleContext context;
	private EventHandler handler;
	private String[] topics;
	private Filter filter;

	/**
	 * Create an EventHandlerWrapper. 
	
	 * @param reference Reference to the EventHandler
	 * @param context Bundle Context of the Event Admin bundle
	 * @param log LogService object for logging
	 */
	public EventHandlerWrapper(ServiceReference<EventHandler> reference, BundleContext context, LogTracker log) {
		this.reference = reference;
		this.context = context;
		this.log = log;
	}

	/**
	 * Cache values from service properties
	 * 
	 * @return true if the handler should be called; false if the handler should not be called
	 */
	public synchronized boolean init() {
		topics = null;
		filter = null;

		// Get topic names
		Object o = reference.getProperty(EventConstants.EVENT_TOPIC);
		if (o instanceof String) {
			topics = new String[] {(String) o};
		} else if (o instanceof String[]) {
			topics = (String[]) o;
		} else if (o instanceof Collection) {
			try {
				@SuppressWarnings("unchecked")
				Collection<String> c = (Collection<String>) o;
				topics = c.toArray(new String[c.size()]);
			} catch (ArrayStoreException e) {
				log.log(LogService.LOG_ERROR, NLS.bind(EventAdminMsg.EVENT_INVALID_HANDLER_TOPICS, o), e);
			}
		}

		if (topics == null) {
			return false;
		}

		// get filter
		o = reference.getProperty(EventConstants.EVENT_FILTER);
		if (o instanceof String) {
			try {
				filter = context.createFilter((String) o);
			} catch (InvalidSyntaxException e) {
				log.log(LogService.LOG_ERROR, NLS.bind(EventAdminMsg.EVENT_INVALID_HANDLER_FILTER, o), e);
				return false;
			}
		}

		return true;
	}

	/**
	 * Flush the handler service if it has been obtained.
	 */
	public void flush() {
		synchronized (this) {
			if (handler == null) {
				return;
			}
			handler = null;
		}
		try {
			context.ungetService(reference);
		} catch (IllegalStateException e) {
			// ignore event admin must have stopped
		}
	}

	/**
	 * Get the event topics for the wrapped handler.
	 * 
	 * @return The wrapped handler's event topics
	 */
	public synchronized String[] getTopics() {
		return topics;
	}

	/**
	 * Return the wrapped handler. 
	 * @return The wrapped handler.
	 */
	private EventHandler getHandler() {
		synchronized (this) {
			// if we already have a handler, return it
			if (handler != null) {
				return handler;
			}
		}

		// we don't have the handler, so lets get it outside the sync region
		EventHandler tempHandler = null;
		try {
			tempHandler = AccessController.doPrivileged(new PrivilegedAction<EventHandler>() {
				@Override
				public EventHandler run() {
					return context.getService(reference);
				}
			});
		} catch (IllegalStateException e) {
			// ignore; event admin may have stopped
		}

		synchronized (this) {
			// do we still need the handler we just got?
			if (handler == null) {
				handler = tempHandler;
				return handler;
			}
			// get the current handler
			tempHandler = handler;
		}

		// unget the handler we just got since we don't need it
		try {
			context.ungetService(reference);
		} catch (IllegalStateException e) {
			// ignore; event admin may have stopped
		}

		// return the current handler (copied into the local var)
		return tempHandler;
	}

	/**
	 * Get the filter object
	 * 
	 * @return The handler's filter
	 */
	private synchronized Filter getFilter() {
		return filter;
	}

	/**
	 * Dispatch event to handler. Perform final tests before actually calling the handler.
	 * 
	 * @param event The event to dispatch
	 * @param perm The permission to be checked
	 */
	public void handleEvent(Event event, Permission perm) {
		Bundle bundle = reference.getBundle();
		// is service unregistered?
		if (bundle == null) {
			return;
		}

		// filter match
		Filter eventFilter = getFilter();
		if ((eventFilter != null) && !event.matches(eventFilter)) {
			return;
		}

		// permission check
		if ((perm != null) && (!bundle.hasPermission(perm))) {
			return;
		}

		// get handler service
		EventHandler handlerService = getHandler();
		if (handlerService == null) {
			return;
		}

		try {
			handlerService.handleEvent(event);
		} catch (Throwable t) {
			if (event.getTopic().startsWith("org/osgi/service/log/LogEntry")) { //$NON-NLS-1$
				Object exception = event.getProperty("exception"); //$NON-NLS-1$
				if (exception instanceof LogTopicException)
					return;// avoid endless event dispatching
				// wrap exception in a LogTopicException to detect endless event dispatching
				t = new LogTopicException(t);
			}
			// log/handle any Throwable thrown by the listener
			log.log(LogService.LOG_ERROR, NLS.bind(EventAdminMsg.EVENT_DISPATCH_HANDLER_EXCEPTION, event, handlerService), t);
		}
	}

	static class LogTopicException extends RuntimeException {
		private static final long serialVersionUID = -2386940335620739632L;

		public LogTopicException(Throwable cause) {
			super(cause);
		}
	}
}
