All Downloads are FREE. Search and download functionalities are using the official Maven repository.

domino.java.OsgiContext Maven / Gradle / Ivy

There is a newer version: 0.3.1
Show newest version
package domino.java;

import static de.tototec.utils.functional.FList.headOption;
import static de.tototec.utils.functional.FList.map;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import org.osgi.framework.Bundle;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Filter;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.ServiceRegistration;
import org.osgi.util.tracker.ServiceTracker;

import de.tototec.utils.functional.F1;
import de.tototec.utils.functional.Optional;
import de.tototec.utils.functional.Procedure1;
import de.tototec.utils.functional.Procedure2;
import de.tototec.utils.functional.Procedure3;
import domino.java.capsule.Capsule;
import domino.java.capsule.CapsuleScope;
import domino.java.capsule.DynamicCapsuleContext;
import domino.java.internal.Logger;
import domino.java.internal.LoggerFactory;

/**
 * This is the main entry point to the Domino Java DSL.
 *
 * By having your bundle activator extend from this class, you get *full*
 * access to the Domino Java DSL.
 * 
 * By calling {@link #whenBundleActive(Procedure1)}, you implicitly create a
 * {@link Capsule} which is tighly coupled to the bundles actived state.
 * All (most) other methods of this class should be called inside this capsule
 * (in the given procedure).
 * 
 * Note that if you use
 * {@link #watchAdvancedServices(Class, String, Procedure1)}, you might
 * additionally want to import the relevant watcher events.
 *
 * == Example 1: Wait for a service
 * 
 * .MyService.java
 * [source,java]
 * ----
 * package org.example.domino_test_one
 * 
 * import org.osgi.service.http.HttpService
 *
 * public class MyService {
 * 
 * private final HttpService httpService;
 * 
 * public MyService(HttpService httpService) {
 * this.httpService = httpService;
 * }
 * }
 * ----
 * 
 * .Activator.java
 * [source,java]
 * ----
 * package org.example.domino_test_one
 * 
 * import domino.java.OsgiContext
 * import org.osgi.service.http.HttpService
 *
 * public class Activator extends DominoActivator {
 * 
 * public Activator() {
 * 
 * whenBundleActive(bundleContext -> {
 * // Make MyService available as long as HttpService is present
 * whenServicePresent(HttpService.class, httpService -> {
 * MyService myService = new MyService(httpService);
 * providesService(myService, MyService.class);
 * });
 * 
 * });
 * 
 * }
 * 
 * }
 * ----
 *
 * FIXME: THis is not yet possible
 * == Example 2: Listen for configuration changes
 *
 * [source,scala]
 * package org.example.domino_test_two
 * 
 * import domino.DominoActivator
 *
 * class KeyService(key: String)
 *
 * class Activator extends DominoActivator {
 * whenBundleActive {
 * // Reregister KeyService whenever configuration changes
 * whenConfigurationActive("my_service") { conf =>
 * val key = conf.get("key") map { _.asInstanceOf[String] } getOrElse
 * "defaultKey"
 * new KeyService(key).providesService[KeyService]
 * }
 * }
 * }
 * ----
 * 
 * TODO: Do not allow per default multiple calls to
 * {@link #whenBundleActive(Procedure1)}, and introduce a new setter to allow
 * overriding {@link #whenBundleActive(Procedure1)}
 *
 */
public class OsgiContext
		extends DynamicCapsuleContext
		implements BundleActivator, ServiceWatching, ServiceProviding, ServiceConsuming {

	private final Logger log = LoggerFactory.getLogger(OsgiContext.class);

	/**
	 * Contains the handler that {@link #whenBundleActive(Procedure1)} has been
	 * called with.
	 */
	private Optional> bundleActiveHandler = Optional.none();

	/**
	 * Contains the bundle context as long as the bundle is active.
	 */
	private Optional bundleContext = Optional.none();

	private Optional bundleActiveCapsuleScope = Optional.none();

	/**
	 * Will be called by the OSGi framework, if you inherit from this class.
	 * 
	 * If you construct this class manually, you have to take care to properly call
	 * the {@link #start(BundleContext)} and {@link #stop(BundleContext)} methods.
	 */
	public OsgiContext() {
	}
	
	/**
	 * Returns `true` as long as the bundle is active and it's bundle context is
	 * valid.
	 */
	public boolean isActive() {
		return bundleContext.isDefined();
	}
	
	/**
	 * Defines a handler `f` to be executed when the bundle becomes active. `f` is
	 * executed as soon as the bundle activator's `start` method is called. This
	 * should be called in the constructor of your activator.
	 *
	 * In `f`, you have the opportunity to add so called capsules, which have their
	 * own `start` and `stop` methods (a kind of mini bundles). Their `stop` methods
	 * will be invoked as soon as the bundle activator's `stop` method is called. So
	 * you have the big chance here to encapsulate start and stop logic at one
	 * place, making the bundle activator less error-prone, better readable and
	 * easier to write.
	 *
	 * @param f
	 *            Handler
	 */
	public void whenBundleActive(final Procedure1 f) {
		// TODO log the caller here
		log.debug("Registering whenBundleActive");

		final boolean firstSet = bundleActiveHandler.isEmpty();

		if (bundleActiveHandler.isDefined()) {
			// TODO: log the caller here
			log.warn("Overriding already present whenBundleActive. The previous whenBundleActive will be ignored");
		}
		bundleActiveHandler = Optional.lift(f);

		// check if we were already started and apply the handler now
		if (firstSet && bundleContext.isDefined()) {
			internalStart();
		}
	}

	@Override
	public void start(final BundleContext context) {
		if (bundleContext.isDefined()) {
			log.error("A BundleContext is already defined. Was the bundle started before? Bundle: {}",
					dumpBundle(context));
		}

		// Make bundle context available in this class
		bundleContext = Optional.lift(context);

		internalStart();
	}

	private void internalStart() {
		bundleContext.foreach(bc -> {
			if (bundleActiveCapsuleScope.isDefined()) {
				log.error("A bundleActiveCapsuleScope is already defined. Was the bundle started before? Bundle: {}",
						dumpBundle(bc));
			}

			// Execute the handler if one was defined
			bundleActiveHandler.foreach(f -> {
				log.debug("Starting whenBundleActive of bundle: {}", dumpBundle(bc));
				// Executes f. All capsules added in f are added to a new
				// capsule
				// scope which is returned afterwards.
				try {
					bundleActiveCapsuleScope = Optional.some(executeWithinNewCapsuleScope(() -> f.apply(bc)));
				} catch (final Throwable e) {
					log.debug("Exception caught while starting whenBundleActive of bundle: {}", dumpBundle(bc), e);
					throw e;
				}
			});
		});
	}

	@Override
	public void stop(final BundleContext context) throws Exception {
		// Stop and release all the capsules in the scope
		try {
			bundleActiveCapsuleScope.foreach(scope -> {
				try {
					log.debug("Stopping whenBundleActive of bundle: {}", dumpBundle(context));
					scope.stop();
				} catch (final Throwable e) {
					log.debug("Exception caught while stopping whenBundleActive of bundle: {}", dumpBundle(context), e);
					throw e;
				} finally {
					bundleActiveCapsuleScope = Optional.none();
				}
			});

		} finally {
			// Release bundle context
			bundleContext = Optional.none();
		}
	}

	/**
	 * Provides convenient `onStop` method which the end user can use for ad-hoc
	 * adding stop logic to the current scope.
	 * 
	 * @param f
	 *            stop logic
	 */
	public void onStop(final Runnable f) {
		// Create a capsule which just contains stop logic
		final Capsule capsule = new Capsule() {

			@Override
			public void start() {
				// nothing to do
			}

			@Override
			public void stop() {
				f.run();
			}
		};

		// Add the capsule to the current scope
		addCapsule(capsule);
	}

	public String dumpBundle(final BundleContext context) {
		final Bundle bundle = context.getBundle();
		return bundle.getSymbolicName() + "[" + bundle.getBundleId() + "]";
	}

	protected  ServiceRegistration internalProvideService(final S service,
			final Iterable> interfaces, final Map properties) {
		if (bundleContext.isEmpty()) {
			throw new IllegalStateException(
					"Cannot provide service. This API method must be called with an valid bundle context.");
		}
		final Optional> reg = bundleContext.flatMap(bc -> {
			final ServiceProviderCapsule spc = new ServiceProviderCapsule<>(interfaces, properties, bc, service);
			addCapsule(spc);
			return spc.serviceRegistration();
		});
		return reg.get();
	}

	//////////////////////////////////
	// Service Providing

	@Override
	public  ServiceRegistration providesService(final S service) {
		@SuppressWarnings("unchecked")
		List> types = Arrays.asList((Class) service.getClass());
		return internalProvideService(service, types, Collections.emptyMap());
	}

	@Override
	public  ServiceRegistration providesService(final S service, final Class type) {
		return internalProvideService(service, Arrays.asList(type), Collections.emptyMap());
	}

	@Override
	public  ServiceRegistration providesService(final S service, final Class type1,
			final Class type2) {
		return internalProvideService(service, Arrays.asList(type1, type2), Collections.emptyMap());
	}

	@Override
	public  ServiceRegistration providesService(final S service, final Class type1,
			final Class type2, final Class type3) {
		return internalProvideService(service, Arrays.asList(type1, type2, type3), Collections.emptyMap());
	}

	@Override
	public  ServiceRegistration providesService(final S service, final Class type1,
			final Class type2, final Class type3, final Class type4) {
		return internalProvideService(service, Arrays.asList(type1, type2, type3, type4), Collections.emptyMap());
	}

	@Override
	public  ServiceRegistration providesService(final S service, final Map properties) {
		@SuppressWarnings("unchecked")
		List> types = Arrays.asList((Class) service.getClass());
		return internalProvideService(service, types, properties);
	}

	@Override
	public  ServiceRegistration providesService(final S service, final Class type,
			final Map properties) {
		return internalProvideService(service, Arrays.asList(type), properties);
	}

	@Override
	public  ServiceRegistration providesService(final S service, final Class type1,
			final Class type2, final Map properties) {
		return internalProvideService(service, Arrays.asList(type1, type2), properties);
	}

	@Override
	public  ServiceRegistration providesService(final S service, final Class type1,
			final Class type2, final Class type3, final Map properties) {
		return internalProvideService(service, Arrays.asList(type1, type2, type3), properties);
	}

	@Override
	public  ServiceRegistration providesService(final S service, final Class type1,
			final Class type2, final Class type3, final Class type4,
			final Map properties) {
		return internalProvideService(service, Arrays.asList(type1, type2, type3, type4), properties);
	}

	//////////////////////////////////
	// Service Watching

	@Override
	public  ServiceTracker watchAdvancedServices(final Class type, final String filter,
			final Procedure1> f) {
		if (bundleContext.isEmpty()) {
			throw new IllegalStateException(
					"Cannot watch service. This API method must be called with an valid bundle context.");
		}
		final Optional> tracker = bundleContext.flatMap(bc -> {
			final String combinedFilter = Util.createCompleteFilter(type, filter);
			Filter typedFilter;
			try {
				typedFilter = bc.createFilter(combinedFilter);
			} catch (final InvalidSyntaxException e) {
				throw new RuntimeException(
						"Could not create valid filter from generated filter string: " + combinedFilter, e);
			}
			final ServiceWatcherCapsule swc = new ServiceWatcherCapsule(typedFilter, f, bc);
			addCapsule(swc);
			return swc.tracker();
		});
		return tracker.get();
	}

	@Override
	public  ServiceTracker whenAdvancedServicePresent(final Class type, final String filter,
			final Procedure1 f) {

		class ActivationState {

			private final S watchedService;
			private final CapsuleScope servicePresentCapsuleScope;

			public ActivationState(final S watchedService, final CapsuleScope servicePresentCapsuleScope) {
				this.watchedService = watchedService;
				this.servicePresentCapsuleScope = servicePresentCapsuleScope;
			}

			public CapsuleScope servicePresentCapsuleScope() {
				return servicePresentCapsuleScope;
			}

			public S watchedService() {
				return watchedService;
			}

			@Override
			public String toString() {
				return getClass().getName() + "(watchedService=" + watchedService + ",servicePresentCapsuleScope="
						+ servicePresentCapsuleScope + ")";
			}

		}

		@SuppressWarnings("unchecked")
		final Optional[] optActivationState = new Optional[] { Optional.none() };

		final ServiceTracker reg = watchAdvancedServices(type, filter, event -> {
			if (event.eventType() == ServiceWatcherEvent.EventType.ADDING) {
				if (optActivationState[0].isEmpty()) {
					// Not already watching a service of this type. Run handler.
					final CapsuleScope newScope = executeWithinNewCapsuleScope(() -> {
						f.apply(event.service());
					});

					// Save the activation state
					optActivationState[0] = Optional.some(new ActivationState(event.service(), newScope));
				}
			} else if (event.eventType() == ServiceWatcherEvent.EventType.REMOVED) {
				optActivationState[0].foreach(activationState -> {
					// Stop the capsule scope only if exactly that service got
					// removed which triggered its creation
					if (event.service() == activationState.watchedService()) {
						activationState.servicePresentCapsuleScope().stop();
						optActivationState[0] = Optional.none();
					}
				});

			}
		});

		return reg;
	}

	@Override
	public  ServiceTracker whenServicePresent(final Class type, final Procedure1 f) {
		return whenAdvancedServicePresent(type, null, f);
	}

	@Override
	public  ServiceTracker whenServicesPresent(final Class type1, final Class type2,
			final Procedure2 f) {
		return whenServicePresent(type1, (final S1 s1) -> {
			whenServicePresent(type2, (final S2 s2) -> {
				f.apply(s1, s2);
			});
		});
	}

	public  ServiceTracker whenServicesPresent(final Class type1, final Class type2,
			final Class type3, final Procedure3 f) {
		return whenServicesPresent(type1, type2, (final S1 s1, final S2 s2) -> {
			whenServicePresent(type3, (final S3 s3) -> {
				f.apply(s1, s2, s3);
			});
		});
	}

	// FIXME re-incomment when procedure 4 becomes available
	// public  ServiceTracker whenServicesPresent(
	// final Class type1,
	// final Class type2,
	// final Class type3,
	// final Class type4,
	// final Procedure4 f) {
	// return whenServicesPresent(type1, type2, type3, (final S1 s1, final S2 s2,
	// final S3 s3) -> {
	// whenServicePresent(type4, (final S4 s4) -> {
	// f.apply(s1, s2, s3, s4);
	// });
	// });
	// }

	/////////////////////////////////
	// Service Consuming

	@Override
	public  R withService(final Class type, final F1, R> f) {
		if (bundleContext.isEmpty()) {
			throw new IllegalStateException(
					"Cannot get service. This API method must be called with an valid bundle context.");
		}

		final BundleContext bc = bundleContext.get();
		final Optional> ref = serviceRef(type);
		if (ref.isDefined()) {
			final S s = bc.getService(ref.get());
			try {
				return f.apply(Optional.some(s));
			} finally {
				bc.ungetService(ref.get());
			}
		} else {
			return f.apply(Optional.none());
		}
	}

	@Override
	public  Optional> serviceRef(final Class type) {
		if (bundleContext.isEmpty()) {
			throw new IllegalStateException(
					"Cannot get service reference. This API method must be called with an valid bundle context.");
		}
		final ServiceReference ref = bundleContext.get().getServiceReference(type.getName());
		return Optional.lift(ref).map(r -> {
			@SuppressWarnings("unchecked")
			final ServiceReference s = (ServiceReference) r;
			return s;
		});
	}

	@Override
	public  Optional> serviceRef(final Class type, final String filter) {
		final Collection> refs = serviceRefs(type, filter);
		return headOption(refs);
	}

	@Override
	public  Collection> serviceRefs(final Class type, final String filter) {
		if (bundleContext.isEmpty()) {
			throw new IllegalStateException(
					"Cannot get service references. This API method must be called with an valid bundle context.");
		}
		final BundleContext bc = bundleContext.get();

		// Get the list of references matching the filter
		Collection> refs = null;
		try {
			refs = bc.getServiceReferences(type, filter);
		} catch (final InvalidSyntaxException e) {
			throw new RuntimeException("Invalid filter syntax: " + filter, e);
		}

		if (refs == null) {
			return Collections.emptyList();
		} else {
			return refs;
		}
	}

	@Override
	public  Optional service(final Class type) {
		if (bundleContext.isEmpty()) {
			throw new IllegalStateException(
					"Cannot get service. This API method must be called with an valid bundle context.");
		}
		return serviceRef(type).map(s -> bundleContext.get().getService(s));
	}

	@Override
	public  Optional service(final Class type, final String filter) {
		if (bundleContext.isEmpty()) {
			throw new IllegalStateException(
					"Cannot get service. This API method must be called with an valid bundle context.");
		}
		return serviceRef(type, filter).map(s -> bundleContext.get().getService(s));
	}

	@Override
	public  List services(final Class type, final String filter) {
		if (bundleContext.isEmpty()) {
			throw new IllegalStateException(
					"Cannot get services. This API method must be called with an valid bundle context.");
		}
		final BundleContext bc = bundleContext.get();
		final Collection> refs = serviceRefs(type, filter);
		return map(refs, ref -> bc.getService(ref));
	}

	@Override
	public  List services(final Class type) {
		return services(type, null);
	}
}