/*
* Copyright 2013 Atteo.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.atteo.moonshine.services;
import java.lang.reflect.Field;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.inject.Provider;
import javax.management.InstanceAlreadyExistsException;
import javax.management.InstanceNotFoundException;
import javax.management.MBeanRegistrationException;
import javax.management.MBeanServer;
import javax.management.MBeanServerFactory;
import javax.management.NotCompliantMBeanException;
import org.atteo.moonshine.ConfigurationException;
import org.atteo.moonshine.injection.InjectMembersModule;
import org.atteo.moonshine.reflection.ReflectionUtils;
import org.atteo.moonshine.services.internal.DuplicateDetectionWrapper;
import org.atteo.moonshine.services.internal.ReflectionTools;
import org.atteo.moonshine.services.internal.ServiceModuleRewriter;
import org.atteo.moonshine.services.internal.ServiceWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.inject.Binding;
import com.google.inject.CreationException;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Module;
import com.google.inject.TypeLiteral;
import com.google.inject.servlet.ServletModule;
import com.google.inject.spi.DefaultElementVisitor;
import com.google.inject.spi.Element;
import com.google.inject.spi.Elements;
import com.google.inject.spi.PrivateElements;
class ServicesImplementation implements Services, Services.Builder {
private final Logger logger = LoggerFactory.getLogger("Moonshine");
private final List<Module> extraModules = new ArrayList<>();
private String applicationName;
private Injector injector;
private Service root;
private final List<LifeCycleListener> listeners = new ArrayList<>();
private List<ServiceWrapper> services;
private MBeanServer mbeanServer;
public ServicesImplementation() {
}
@Override
public Builder applicationName(String applicationName) {
this.applicationName = applicationName;
return this;
}
@Override
public Builder addModule(Module module) {
extraModules.add(module);
return this;
}
@Override
public Builder configuration(Service root) {
this.root = root;
return this;
}
@Override
public Builder registerListener(LifeCycleListener listener) {
listeners.add(listener);
return this;
}
@Override
public Services build() throws ConfigurationException {
if (root == null) {
root = new AbstractService() {
};
}
buildInjector();
return this;
}
private void buildInjector() throws ConfigurationException {
List<Module> modules = new ArrayList<>();
DuplicateDetectionWrapper duplicateDetection = new DuplicateDetectionWrapper();
// Use ServletModule specifically so @RequestScoped annotation will be always bound
Module servletsModule = duplicateDetection.wrap(new ServletModule() {
@Override
public void configureServlets() {
binder().requireExplicitBindings();
binder().bind(new TypeLiteral<List<? extends ServiceInfo>>() {})
.toProvider(() -> {
return getServiceElements();
});
}
});
// important magic below:
// Every ServletModule instance tries to install InternalServletModule. The trick is used, because Guice
// installs modules only the first time and ignores any subsequent execution of install method
// with the same module (by comparing them using equals() method).
// We need to make sure InternalServletModule is installed in the top level module,
// because when it is installed from some private module it doesn't have an access to all
// registered servlets and filters.
//
// The line below makes sure the first instance of ServletModule is rewritten in global scope
modules.add(Elements.getModule(Elements.getElements(servletsModule)));
for (Module module : extraModules) {
modules.add(duplicateDetection.wrap(module));
}
services = readServiceMetadata(retrieveServicesRecursively(root));
services = sortTopologically(services);
verifySingletonServicesAreUnique(services);
createMBeanServer();
registerInJMX();
List<String> hints = new ArrayList<>();
try {
for (ServiceWrapper service : services) {
Module module = service.configure();
if (module != null) {
service.setElements(Elements.getElements(duplicateDetection.wrap(module)));
} else {
service.setElements(Collections.<com.google.inject.spi.Element>emptyList());
}
}
for (ServiceWrapper service : services) {
checkOnlySingletonBindWithoutAnnotation(service);
}
for (ServiceWrapper service : services) {
service.setElements(ServiceModuleRewriter.annotateExposedWithId(service.getElements(),
service.getService()));
}
for (ServiceWrapper service : services) {
service.setElements(ServiceModuleRewriter.importBindings(service, services, hints));
}
for (ServiceWrapper service : services) {
modules.add(Elements.getModule(service.getElements()));
}
modules.add(new InjectMembersModule());
logger.info("Creating injector");
injector = Guice.createInjector(modules);
for (LifeCycleListener listener : listeners) {
listener.configured(getGlobalInjector());
}
} catch (CreationException e) {
if (!hints.isEmpty()) {
logger.warn("Problem detected while creating Guice injector, possible causes:");
for (String hint : hints) {
logger.warn(" -> " + hint);
}
}
try {
close();
} catch (Exception f) {
e.addSuppressed(f);
}
throw e;
} catch (RuntimeException e) {
try {
close();
} catch (Exception f) {
e.addSuppressed(f);
}
throw e;
}
}
private void createMBeanServer() {
mbeanServer = MBeanServerFactory.createMBeanServer(applicationName);
}
private void registerInJMX() throws ConfigurationException {
try {
for (ServiceWrapper service : services) {
mbeanServer.registerMBean(service, null);
}
} catch (InstanceAlreadyExistsException | MBeanRegistrationException | NotCompliantMBeanException e) {
throw new RuntimeException(e);
}
}
private void unregisterFromJMX() {
for (ServiceWrapper service : services) {
try {
mbeanServer.unregisterMBean(service.getObjectName());
} catch (InstanceNotFoundException | MBeanRegistrationException e) {
throw new RuntimeException(e);
}
}
}
@Override
public Injector getGlobalInjector() {
return injector;
}
@Override
public void start() {
logger.info("Starting services");
for (ServiceWrapper service : services) {
service.start();
}
logger.info("All services started");
for (LifeCycleListener listener : listeners) {
listener.started();
}
}
@Override
public void stop() {
for (LifeCycleListener listener : listeners) {
listener.stopping();
}
for (ServiceWrapper service : Lists.reverse(services)) {
service.stop();
}
}
@Override
public void close() {
unregisterFromJMX();
stop();
for (LifeCycleListener listener : listeners) {
listener.closing();
}
for (ServiceWrapper service : Lists.reverse(services)) {
service.close();
}
if (logger != null) {
logger.info("All services stopped");
}
injector = null;
}
@Override
public List<? extends ServiceWrapper> getServiceElements() {
return services;
}
private static void verifySingletonServicesAreUnique(List<ServiceWrapper> services) throws ConfigurationException {
Set<Class<?>> set = new HashSet<>();
for (ServiceWrapper service : services) {
Class<?> klass = service.getService().getClass();
if (service.isSingleton()) {
if (set.contains(klass)) {
throw new ConfigurationException("Service '" + service.getName() + "' is marked"
+ " as singleton, but is declared more than once in configuration file");
}
set.add(klass);
if (!Strings.isNullOrEmpty(service.getService().getId())) {
throw new ConfigurationException("Service '" + service.getName() + "' is marked"
+ " as singleton, but has an id specified");
}
}
}
}
private static void ensureBindsWithoutAnnotation(Element element, final ServiceWrapper service) {
element.acceptVisitor(new DefaultElementVisitor<Void>() {
@Override
public <T> Void visit(Binding<T> binding) {
if (binding.getKey().getAnnotation() != null) {
throw new IllegalStateException("Non singleton service " + service.getName()
+ " cannot bind " + binding.toString() + ". Only services marked with @Singleton"
+ " can bind with annotation.");
}
return null;
}
@Override
public Void visit(PrivateElements privateElements) {
for (Key<?> key : privateElements.getExposedKeys()) {
if (key.getAnnotation() != null) {
throw new IllegalStateException("Non singleton service " + service.getName()
+ " cannot expose " + key.toString() + ". Only services marked with @Singleton"
+ " can expose bindings with annotation.");
}
}
return null;
}
});
}
private static void checkOnlySingletonBindWithoutAnnotation(final ServiceWrapper service) {
if (service.isSingleton()) {
return;
}
for (Element element : service.getElements()) {
ensureBindsWithoutAnnotation(element, service);
}
}
/**
* Finds dependencies between services.
*/
private List<ServiceWrapper> readServiceMetadata(List<Service> services) throws ConfigurationException {
List<ServiceWrapper> servicesMetadata = new ArrayList<>();
Map<Service, ServiceWrapper> map = new IdentityHashMap<>();
for (Service service : services) {
ServiceWrapper metadata = new ServiceWrapper(service);
servicesMetadata.add(metadata);
map.put(service, metadata);
}
List<String> configurationErrors = new ArrayList<>();
for (ServiceWrapper metadata : servicesMetadata) {
for (Class<?> ancestorClass : ReflectionUtils.getAncestors(metadata.getService().getClass())) {
metadata.setSingleton(ReflectionTools.isSingleton(ancestorClass));
for (final Field field : ancestorClass.getDeclaredFields()) {
ImportService importAnnotation = field.getAnnotation(ImportService.class);
if (importAnnotation == null) {
continue;
}
if (!Service.class.isAssignableFrom(field.getType())) {
throw new RuntimeException("@" + ImportService.class.getSimpleName() + " annotation can only"
+ " be specified on a field of type " + Service.class.getSimpleName());
}
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
field.setAccessible(true);
return null;
});
Service importedService;
try {
importedService = (Service) field.get(metadata.getService());
ServiceWrapper importedServiceMetadata;
if (importedService == null) {
try {
importedServiceMetadata = findDefaultService(servicesMetadata, field.getType());
} catch (ConfigurationException ex) {
configurationErrors.add("Service '" + metadata.getName()
+ "' requires '" + field.getType().getName() + "' which is"
+ " defined more than once. Please specify an ID in your"
+ " configuration files.");
continue;
}
if (importedServiceMetadata == null) {
configurationErrors.add("Service '" + metadata.getName()
+ "' requires '" + field.getType().getName() + "' which was"
+ " not found. Please check your configuration files.");
continue;
}
field.set(metadata.getService(), importedServiceMetadata.getService());
} else {
importedServiceMetadata = map.get(importedService);
if (importedServiceMetadata == null) {
throw new RuntimeException("Unknown service imported");
}
}
metadata.addDependency(importedServiceMetadata, importAnnotation.bindWith());
} catch (IllegalAccessException | IllegalArgumentException e) {
throw new RuntimeException("Cannot access field", e);
}
}
}
}
if (!configurationErrors.isEmpty()) {
if (configurationErrors.size() == 1) {
throw new ConfigurationException(configurationErrors.get(0));
} else {
for (String error : configurationErrors) {
logger.error(error);
}
throw new ConfigurationException("Multiple configuration errors");
}
}
return servicesMetadata;
}
/**
* Find the default service which can be assigned for given type.
*/
private static ServiceWrapper findDefaultService(List<ServiceWrapper> services, Class<?> type) throws ConfigurationException {
ServiceWrapper result = null;
for (ServiceWrapper service : services) {
if (type.isAssignableFrom(service.getService().getClass())) {
if (result != null) {
throw new ConfigurationException("Service of type '" + type + "' is not unique");
}
result = service;
}
}
return result;
}
private static List<Service> retrieveServicesRecursively(Service service) {
List<Service> result = new ArrayList<>();
addServicesRecursively(result, service);
return result;
}
private static void addServicesRecursively(List<Service> result, Service service) {
result.add(service);
for (Service subService : service.getSubServices()) {
addServicesRecursively(result, subService);
}
}
private static void handleCycle(ServiceWrapper service, List<ServiceWrapper> chain) {
boolean cycleStarted = false;
StringBuilder builder = new StringBuilder();
for (ServiceWrapper serviceWrapper : chain) {
if (serviceWrapper == service) {
cycleStarted = true;
}
if (cycleStarted) {
builder.append(serviceWrapper.getName());
builder.append(" -> ");
}
}
builder.append(service.getName());
throw new RuntimeException("Service " + service.getName() + " depends on itself: "
+ builder.toString());
}
private static void addService(ServiceWrapper service, Set<ServiceWrapper> set, List<ServiceWrapper> chain,
List<ServiceWrapper> sorted) {
// check for cycles
if (chain.contains(service)) {
handleCycle(service, chain);
}
if (!set.contains(service)) {
return;
}
chain.add(service);
for (ServiceWrapper.Dependency dependency : service.getDependencies()) {
addService(dependency.getService(), set, chain, sorted);
}
chain.remove(service);
set.remove(service);
sorted.add(service);
}
private static List<ServiceWrapper> sortTopologically(List<ServiceWrapper> services) {
List<ServiceWrapper> sorted = new ArrayList<>();
Set<ServiceWrapper> set = new LinkedHashSet<>(services);
while (!set.isEmpty()) {
ServiceWrapper service = set.iterator().next();
addService(service, set, new ArrayList<>(), sorted);
}
return sorted;
}
}