/* * Hibernate Search, full-text search for your domain model * * License: GNU Lesser General Public License (LGPL), version 2.1 or later * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>. */ package org.hibernate.search.engine.service.impl; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.hibernate.search.cfg.spi.SearchConfiguration; import org.hibernate.search.engine.service.beanresolver.impl.ReflectionBeanResolver; import org.hibernate.search.engine.service.beanresolver.impl.ReflectionFallbackBeanResolver; import org.hibernate.search.engine.service.beanresolver.spi.BeanResolver; import org.hibernate.search.engine.service.classloading.spi.ClassLoaderService; import org.hibernate.search.engine.service.spi.Service; import org.hibernate.search.engine.service.spi.ServiceManager; import org.hibernate.search.engine.service.spi.ServiceReference; import org.hibernate.search.engine.service.spi.Startable; import org.hibernate.search.engine.service.spi.Stoppable; import org.hibernate.search.exception.AssertionFailure; import org.hibernate.search.spi.BuildContext; import org.hibernate.search.util.StringHelper; import org.hibernate.search.util.impl.ClassLoaderHelper; import org.hibernate.search.util.logging.impl.Log; import org.hibernate.search.util.logging.impl.LoggerFactory; /** * Default implementation of the {@code ServiceManager} interface. * * @author Emmanuel Bernard * @author Sanne Grinovero * @author Hardy Ferentschik */ public class StandardServiceManager implements ServiceManager { private static final Log log = LoggerFactory.make(); private final Properties properties; private final BuildContext buildContext; private final ConcurrentHashMap<Class<?>, ServiceWrapper<?>> cachedServices = new ConcurrentHashMap<Class<?>, ServiceWrapper<?>>(); private final Map<Class<? extends Service>, Object> providedServices; private final Map<Class<? extends Service>, String> defaultServices; private final ClassLoaderService classloaderService; private final BeanResolver beanResolver; private final boolean failOnUnreleasedService; private volatile boolean allServicesReleased = false; public StandardServiceManager(SearchConfiguration searchConfiguration, BuildContext buildContext) { this( searchConfiguration, buildContext, Collections.<Class<? extends Service>, String>emptyMap() ); } public StandardServiceManager(SearchConfiguration searchConfiguration, BuildContext buildContext, Map<Class<? extends Service>, String> defaultServices) { this.buildContext = buildContext; this.properties = searchConfiguration.getProperties(); this.defaultServices = defaultServices; this.classloaderService = searchConfiguration.getClassLoaderService(); BeanResolver configuredBeanResolver = searchConfiguration.getBeanResolver(); ReflectionBeanResolver reflectionBeanResolver = new ReflectionBeanResolver(); if ( configuredBeanResolver != null ) { this.beanResolver = new ReflectionFallbackBeanResolver( configuredBeanResolver, reflectionBeanResolver ); } else { this.beanResolver = reflectionBeanResolver; } this.providedServices = createProvidedServices( searchConfiguration ); // Requires beanResolver and classloaderService to be set this.failOnUnreleasedService = Boolean.getBoolean( "org.hibernate.search.fail_on_unreleased_service" ); } @Override @SuppressWarnings("unchecked") public <S extends Service> S requestService(Class<S> serviceRole) { if ( serviceRole == null ) { throw new IllegalArgumentException( "'null' is not a valid service role" ); } if ( allServicesReleased ) { throw log.serviceRequestedAfterReleasedAllWasCalled(); } // provided services have priority over managed services final Object providedService = providedServices.get( serviceRole ); if ( providedService != null ) { return (S) providedService; } ServiceWrapper<S> wrapper = (ServiceWrapper<S>) cachedServices.get( serviceRole ); if ( wrapper == null ) { wrapper = createAndCacheWrapper( serviceRole ); } wrapper.startVirtual(); return wrapper.getService(); } @Override public <S extends Service> ServiceReference<S> requestReference(Class<S> serviceRole) { return new ServiceReference<>( this, serviceRole ); } @Override public <S extends Service> void releaseService(Class<S> serviceRole) { if ( serviceRole == null ) { throw new IllegalArgumentException( "'null' is not a valid service role" ); } if ( providedServices.containsKey( serviceRole ) ) { return; } ServiceWrapper wrapper = cachedServices.get( serviceRole ); if ( wrapper != null ) { wrapper.stopVirtual(); } } @Override public synchronized void releaseAllServices() { for ( ServiceWrapper<?> wrapper : cachedServices.values() ) { /* * Perform an additional stopVirtual, to remove the extra usage token granted at first initialization, * which keeps the service to be really stopped when it's released by the service client, yet we're not shutting down * the Search engine yet. */ wrapper.stopVirtual(); } /* * If everything went well, the previous pass should have brought every * user count to 0 for every service, so every service should have been stopped. * This should also have chained-released services used by services, which * ultimately should have stopped everything. */ /* * Second pass to check for still-running services and forcefully stop them. */ List<String> unreleasedServicesToReport = failOnUnreleasedService ? new ArrayList<String>() : null; for ( ServiceWrapper<?> wrapper : cachedServices.values() ) { synchronized ( wrapper ) { if ( wrapper.status != ServiceStatus.STOPPED ) { log.serviceProviderNotReleased( wrapper.serviceClass ); wrapper.stopReal(); if ( unreleasedServicesToReport != null ) { unreleasedServicesToReport.add( wrapper.serviceClass.getName() ); } } } } cachedServices.clear(); allServicesReleased = true; if ( failOnUnreleasedService && !unreleasedServicesToReport.isEmpty() ) { throw new AssertionFailure( "The following services have been used but not released: " + unreleasedServicesToReport ); } } private Map<Class<? extends Service>, Object> createProvidedServices(SearchConfiguration searchConfiguration) { Map<Class<? extends Service>, Object> tmpServices = new HashMap<Class<? extends Service>, Object>( searchConfiguration.getProvidedServices() ); if ( tmpServices.containsKey( ClassLoaderService.class ) ) { throw log.classLoaderServiceContainedInProvidedServicesException(); } else { tmpServices.put( ClassLoaderService.class, this.classloaderService ); } if ( tmpServices.containsKey( BeanResolver.class ) ) { throw log.beanResolverContainedInProvidedServicesException(); } else { tmpServices.put( BeanResolver.class, this.beanResolver ); } return Collections.unmodifiableMap( tmpServices ); } /** * The 'synchronized' is necessary to avoid loading the same service in parallel: enumerating service * implementations is not threadsafe when delegating to Hibernate ORM's org.hibernate.boot.registry.classloading.spi.ClassLoaderService */ private synchronized <S extends Service> ServiceWrapper<S> createAndCacheWrapper(Class<S> serviceRole) { //Check again, for concurrent usage: ServiceWrapper<S> existingWrapper = (ServiceWrapper<S>) cachedServices.get( serviceRole ); if ( existingWrapper != null ) { return existingWrapper; } Set<S> services = new HashSet<>(); for ( S service : requestService( ClassLoaderService.class ).loadJavaServices( serviceRole ) ) { services.add( service ); } if ( services.size() == 0 ) { tryLoadingDefaultService( serviceRole, services ); } else if ( services.size() > 1 ) { throw log.getMultipleServiceImplementationsException( serviceRole.toString(), StringHelper.join( services, "," ) ); } S service = services.iterator().next(); ServiceWrapper<S> wrapper = new ServiceWrapper<S>( service, serviceRole, buildContext ); @SuppressWarnings("unchecked") ServiceWrapper<S> previousWrapper = (ServiceWrapper<S>) cachedServices.putIfAbsent( serviceRole, wrapper ); if ( previousWrapper != null ) { wrapper = previousWrapper; } else { //Initialize the service usage counter with an additional usage token, on top of the one granted by the service request: wrapper.startVirtual(); } return wrapper; } private <S extends Service> void tryLoadingDefaultService(Class<S> serviceRole, Set<S> services) { // there is no loadable service. Check whether we have a default one we can instantiate if ( defaultServices.containsKey( serviceRole ) ) { S service = ClassLoaderHelper.instanceFromName( serviceRole, defaultServices.get( serviceRole ), "default service", this ); services.add( service ); } else { throw log.getNoServiceImplementationFoundException( serviceRole.toString() ); } } private class ServiceWrapper<S> { private final S service; private final BuildContext context; private final Class<S> serviceClass; private int userCounter = 0; private ServiceStatus status = ServiceStatus.STOPPED; ServiceWrapper(S service, Class<S> serviceClass, BuildContext context) { this.service = service; this.context = context; this.serviceClass = serviceClass; } synchronized S getService() { if ( status != ServiceStatus.RUNNING ) { stateExpectedFailure(); } return service; } /** * Virtual call to the start method: only actually starts the * service when bumping up the counter of start requests from * zero. Subsequent start requests will simply increment the * counter, so that we can eventually tear down the services * in reverse order to make dependency graphs happy. * * Make sure to invoke startVirtual() both on service request, * and on creation of the wrapper so that the first request * accounts for two usage tokens rather than one. * The shutdown process will similarly invoke stopVirtual() an * additional time; this is to prevent services from starting * and stopping frequently at runtime. */ synchronized void startVirtual() { int previousValue = userCounter; userCounter++; if ( previousValue == 0 ) { if ( status != ServiceStatus.STOPPED ) { stateExpectedFailure(); } startService( service ); } if ( status != ServiceStatus.RUNNING ) { //Could happen on circular dependencies stateExpectedFailure(); } } synchronized void stopVirtual() { userCounter--; if ( userCounter == 0 ) { //Do not check for the expected status in this case: we don't want a previous service start failure to //prevent us to further attempt to stop services. stopReal(); } } synchronized void stopReal() { status = ServiceStatus.STOPPING; try { if ( service instanceof Stoppable ) { ( (Stoppable) service ).stop(); } } catch (Exception e) { log.stopServiceFailed( serviceClass, e ); } finally { status = ServiceStatus.STOPPED; } } private void startService(final S service) { status = ServiceStatus.STARTING; if ( service instanceof Startable ) { ( (Startable) service ).start( properties, context ); } status = ServiceStatus.RUNNING; } private void stateExpectedFailure() { throw log.getUnexpectedServiceStatusException( status.name(), service.toString() ); } } private enum ServiceStatus { RUNNING, STOPPED, STARTING, STOPPING } @Override public ClassLoaderService getClassLoaderService() { return classloaderService; } @Override public BeanResolver getBeanResolver() { return beanResolver; } }