/** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.apache.isis.core.metamodel.services; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import javax.inject.Inject; import com.google.common.base.Predicate; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Iterables; import com.google.common.collect.ListMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.isis.applib.annotation.Programmatic; import org.apache.isis.applib.services.exceprecog.ExceptionRecognizer; import org.apache.isis.applib.services.publish.PublishingService; import org.apache.isis.core.commons.authentication.AuthenticationSessionProvider; import org.apache.isis.core.commons.components.ApplicationScopedComponent; import org.apache.isis.core.commons.config.IsisConfiguration; import org.apache.isis.core.commons.config.IsisConfigurationDefault; import org.apache.isis.core.commons.util.ToString; import org.apache.isis.core.metamodel.deployment.DeploymentCategory; import org.apache.isis.core.metamodel.deployment.DeploymentCategoryProvider; import org.apache.isis.core.metamodel.exceptions.MetaModelException; import org.apache.isis.core.metamodel.services.configinternal.ConfigurationServiceInternal; import org.apache.isis.core.metamodel.services.persistsession.PersistenceSessionServiceInternal; import org.apache.isis.core.metamodel.spec.InjectorMethodEvaluator; import org.apache.isis.core.metamodel.specloader.InjectorMethodEvaluatorDefault; import org.apache.isis.core.metamodel.specloader.SpecificationLoader; import org.apache.isis.core.runtime.authentication.AuthenticationManager; import org.apache.isis.core.runtime.authorization.AuthorizationManager; /** * The repository of services, also able to inject into any object. * * <p> * Implementation is (and must be) a thread-safe. * </p> * */ public class ServicesInjector implements ApplicationScopedComponent { private static final Logger LOG = LoggerFactory.getLogger(ServicesInjector.class); public static final String KEY_SET_PREFIX = "isis.services.injector.setPrefix"; public static final String KEY_INJECT_PREFIX = "isis.services.injector.injectPrefix"; //region > constructor, fields /** * This is mutable internally, but only ever exposed (in {@link #getRegisteredServices()}) as immutable. */ private final List<Object> services = Lists.newArrayList(); /** * If no key, not yet searched for type; otherwise the corresponding value is a {@link List} of all * services that are assignable to the type. It's possible that this is an empty list. */ private final Map<Class<?>, List<Object>> servicesAssignableToType = Maps.newHashMap(); private final Map<Class<?>, Object> serviceByConcreteType = Maps.newHashMap(); private final InjectorMethodEvaluator injectorMethodEvaluator; private final boolean autowireSetters; private final boolean autowireInject; public ServicesInjector(final List<Object> services, final IsisConfiguration configuration) { this(services, null, configuration); } /** * For testing. */ public ServicesInjector( final List<Object> services, final IsisConfigurationDefault configuration, final InjectorMethodEvaluator injectorMethodEvaluator) { this(services, injectorMethodEvaluator, defaultAutowiring(configuration)); } private static IsisConfiguration defaultAutowiring(final IsisConfigurationDefault configuration) { configuration.put(KEY_SET_PREFIX, ""+true); configuration.put(KEY_INJECT_PREFIX, ""+false); return configuration; } /** * For testing. */ private ServicesInjector( final List<Object> services, final InjectorMethodEvaluator injectorMethodEvaluator, final IsisConfiguration configuration) { this.services.addAll(services); this.injectorMethodEvaluator = injectorMethodEvaluator != null ? injectorMethodEvaluator : new InjectorMethodEvaluatorDefault(); this.autowireSetters = configuration.getBoolean(KEY_SET_PREFIX, true); this.autowireInject = configuration.getBoolean(KEY_INJECT_PREFIX, false); } //endregion //region > replaceServices /** * Update an individual service. * * <p> * There should already be a service {@link #getRegisteredServices() registered} of the specified type. * * @return <tt>true</tt> if a service of the specified type was found and updated, <tt>false</tt> otherwise. * @param existingService * @param replacementService */ public <T> void replaceService(final T existingService, final T replacementService) { if(!services.remove(existingService)) { throw new IllegalArgumentException("Service to be replaced was not found (" + existingService + ")"); } services.add(replacementService); // invalidate servicesAssignableToType.clear(); serviceByConcreteType.clear(); autowire(); } public boolean isRegisteredService(final Class<?> cls) { // lazily construct cache if(serviceByConcreteType.isEmpty()) { for (Object service : services) { final Class<?> concreteType = service.getClass(); serviceByConcreteType.put(concreteType, service); } } return serviceByConcreteType.containsKey(cls); } public <T> void addFallbackIfRequired(final Class<T> serviceClass, final T serviceInstance) { if(!contains(services, serviceClass)) { // add to beginning; // (when first introduced, this feature has been used for the // FixtureScriptsDefault so that appears it top of prototyping menu; not // more flexible than this currently just because of YAGNI). services.add(0, serviceInstance); } } /** * Validate domain service Ids are unique. */ public void validateServices() { validate(getRegisteredServices()); } private static void validate(List<Object> serviceList) { ListMultimap<String, Object> servicesById = ArrayListMultimap.create(); for (Object service : serviceList) { String id = ServiceUtil.id(service); servicesById.put(id, service); } for (Map.Entry<String, Collection<Object>> servicesForId : servicesById.asMap().entrySet()) { String serviceId = servicesForId.getKey(); Collection<Object> services = servicesForId.getValue(); if(services.size() > 1) { throw new IllegalStateException( String.format("Service ids must be unique; serviceId '%s' is declared by domain services %s", serviceId, classNamesFor(services))); } } } private static String classNamesFor(Collection<Object> services) { StringBuilder buf = new StringBuilder(); for (Object service : services) { if(buf.length() > 0) { buf.append(", "); } buf.append(service.getClass().getName()); } return buf.toString(); } static boolean contains(final List<Object> services, final Class<?> serviceClass) { for (Object service : services) { if(serviceClass.isAssignableFrom(service.getClass())) { return true; } } return false; } /** * All registered services, as an immutable {@link List}. */ public List<Object> getRegisteredServices() { return Collections.unmodifiableList(services); } //endregion //region > injectServicesInto /** * Provided by the <tt>ServicesInjector</tt> when used by framework. * * <p> * Called in multiple places from metamodel and facets. */ public void injectServicesInto(final Object object) { injectServices(object, services); } /** * As per {@link #injectServicesInto(Object)}, but for all objects in the * list. */ public void injectServicesInto(final List<Object> objects) { for (final Object object : objects) { injectInto(object); // if implements ServiceInjectorAware injectServicesInto(object); // via @javax.inject.Inject or setXxx(...) } } //endregion //region > injectInto /** * That is, injecting this injector... */ public void injectInto(final Object candidate) { if (ServicesInjectorAware.class.isAssignableFrom(candidate.getClass())) { final ServicesInjectorAware cast = ServicesInjectorAware.class.cast(candidate); cast.setServicesInjector(this); } } //endregion //region > helpers private void injectServices(final Object object, final List<Object> services) { final Class<?> cls = object.getClass(); autowireViaFields(object, services, cls); if(autowireSetters) { autowireViaPrefixedMethods(object, services, cls, "set"); } if(autowireInject) { autowireViaPrefixedMethods(object, services, cls, "inject"); } } private void autowireViaFields(final Object object, final List<Object> services, final Class<?> cls) { final List<Field> fields = Arrays.asList(cls.getDeclaredFields()); final Iterable<Field> injectFields = Iterables.filter(fields, new Predicate<Field>() { @Override public boolean apply(final Field input) { final Inject annotation = input.getAnnotation(javax.inject.Inject.class); return annotation != null; } }); for (final Field field : injectFields) { autowire(object, field, services); } // recurse up the object's class hierarchy final Class<?> superclass = cls.getSuperclass(); if(superclass != null) { autowireViaFields(object, services, superclass); } } private void autowire( final Object object, final Field field, final List<Object> services) { final Class<?> type = field.getType(); // don't think that type can ever be null, // but Javadoc for java.lang.reflect.Field doesn't say if(type == null) { return; } // inject into Collection<T> or List<T> if(Collection.class.isAssignableFrom(type) || List.class.isAssignableFrom(type)) { final Type genericType = field.getGenericType(); if(genericType instanceof ParameterizedType) { final ParameterizedType listParameterizedType = (ParameterizedType) genericType; final Class<?> listType = (Class<?>) listParameterizedType.getActualTypeArguments()[0]; final List<Object> listOfServices = Collections.unmodifiableList( Lists.newArrayList( Iterables.filter(services, new Predicate<Object>() { @Override public boolean apply(final Object input) { return input != null && listType.isAssignableFrom(input.getClass()); } }))); invokeInjectorField(field, object, listOfServices); } } for (final Object service : services) { final Class<?> serviceClass = service.getClass(); if(type.isAssignableFrom(serviceClass)) { invokeInjectorField(field, object, service); return; } } } private void autowireViaPrefixedMethods( final Object object, final List<Object> services, final Class<?> cls, final String prefix) { final List<Method> methods = Arrays.asList(cls.getMethods()); final Iterable<Method> prefixedMethods = Iterables.filter(methods, new Predicate<Method>(){ public boolean apply(final Method method) { final String methodName = method.getName(); return methodName.startsWith(prefix); } }); for (final Method prefixedMethod : prefixedMethods) { autowire(object, prefixedMethod, services); } } private void autowire( final Object object, final Method prefixedMethod, final List<Object> services) { for (final Object service : services) { final Class<?> serviceClass = service.getClass(); final boolean isInjectorMethod = injectorMethodEvaluator.isInjectorMethodFor(prefixedMethod, serviceClass); if(isInjectorMethod) { prefixedMethod.setAccessible(true); invokeInjectorMethod(prefixedMethod, object, service); return; } } } private static void invokeMethod(final Method method, final Object target, final Object[] parameters) { try { method.invoke(target, parameters); } catch (final SecurityException | IllegalAccessException e) { throw new MetaModelException(String.format("Cannot access the %s method in %s", method.getName(), target.getClass().getName())); } catch (final IllegalArgumentException e1) { throw new MetaModelException(e1); } catch (final InvocationTargetException e) { final Throwable targetException = e.getTargetException(); if (targetException instanceof RuntimeException) { throw (RuntimeException) targetException; } else { throw new MetaModelException(targetException); } } } private static void invokeInjectorField(final Field field, final Object target, final Object parameter) { try { field.setAccessible(true); field.set(target, parameter); } catch (final IllegalArgumentException e) { throw new MetaModelException(e); } catch (final IllegalAccessException e) { throw new MetaModelException(String.format("Cannot access the %s field in %s", field.getName(), target.getClass().getName())); } if (LOG.isDebugEnabled()) { LOG.debug("injected " + parameter + " into " + new ToString(target)); } } private static void invokeInjectorMethod(final Method method, final Object target, final Object parameter) { final Object[] parameters = new Object[] { parameter }; invokeMethod(method, target, parameters); if (LOG.isDebugEnabled()) { LOG.debug("injected " + parameter + " into " + new ToString(target)); } } //endregion //region > autoWire @Programmatic public void autowire() { injectServicesInto(this.services); } //endregion //region > lookupService, lookupServices /** * Returns the first registered domain service implementing the requested type. * * <p> * Typically there will only ever be one domain service implementing a given type, * (eg {@link PublishingService}), but for some services there can be more than one * (eg {@link ExceptionRecognizer}). * * @see #lookupServices(Class) */ @Programmatic public <T> T lookupService(final Class<T> serviceClass) { final List<T> services = lookupServices(serviceClass); return !services.isEmpty() ? services.get(0) : null; } @Programmatic public <T> T lookupServiceElseFail(final Class<T> serviceClass) { T service = lookupService(serviceClass); if(service == null) { throw new IllegalStateException("Could not locate service of type '" + serviceClass + "'"); } return service; } /** * Returns all domain services implementing the requested type, in the order * that they were registered in <tt>isis.properties</tt>. * * <p> * Typically there will only ever be one domain service implementing a given type, * (eg {@link PublishingService}), but for some services there can be more than one * (eg {@link ExceptionRecognizer}). * * @see #lookupService(Class) */ @SuppressWarnings("unchecked") @Programmatic public <T> List<T> lookupServices(final Class<T> serviceClass) { locateAndCache(serviceClass); return Collections.unmodifiableList((List<T>) servicesAssignableToType.get(serviceClass)); }; private void locateAndCache(final Class<?> serviceClass) { if(servicesAssignableToType.containsKey(serviceClass)) { return; } final List<Object> matchingServices = Lists.newArrayList(); addAssignableTo(serviceClass, services, matchingServices); servicesAssignableToType.put(serviceClass, matchingServices); } private static void addAssignableTo(final Class<?> type, final List<Object> candidates, final List<Object> filteredServicesAndContainer) { final Iterable<Object> filteredServices = Iterables.filter(candidates, ofType(type)); filteredServicesAndContainer.addAll(Lists.newArrayList(filteredServices)); } private static final Predicate<Object> ofType(final Class<?> cls) { return new Predicate<Object>() { @Override public boolean apply(final Object input) { return cls.isAssignableFrom(input.getClass()); } }; } //endregion //region > convenience lookups (singletons only, cached) private AuthenticationManager authenticationManager; @Programmatic public AuthenticationManager getAuthenticationManager() { return authenticationManager != null ? authenticationManager : (authenticationManager = lookupServiceElseFail(AuthenticationManager.class)); } private AuthorizationManager authorizationManager; @Programmatic public AuthorizationManager getAuthorizationManager() { return authorizationManager != null ? authorizationManager : (authorizationManager = lookupServiceElseFail(AuthorizationManager.class)); } private SpecificationLoader specificationLoader; @Programmatic public SpecificationLoader getSpecificationLoader() { return specificationLoader != null ? specificationLoader : (specificationLoader = lookupServiceElseFail(SpecificationLoader.class)); } private AuthenticationSessionProvider authenticationSessionProvider; @Programmatic public AuthenticationSessionProvider getAuthenticationSessionProvider() { return authenticationSessionProvider != null ? authenticationSessionProvider : (authenticationSessionProvider = lookupServiceElseFail(AuthenticationSessionProvider.class)); } private PersistenceSessionServiceInternal persistenceSessionServiceInternal; @Programmatic public PersistenceSessionServiceInternal getPersistenceSessionServiceInternal() { return persistenceSessionServiceInternal != null ? persistenceSessionServiceInternal : (persistenceSessionServiceInternal = lookupServiceElseFail(PersistenceSessionServiceInternal.class)); } private ConfigurationServiceInternal configurationServiceInternal; @Programmatic public ConfigurationServiceInternal getConfigurationServiceInternal() { return configurationServiceInternal != null ? configurationServiceInternal : (configurationServiceInternal = lookupServiceElseFail(ConfigurationServiceInternal.class)); } private DeploymentCategoryProvider deploymentCategoryProvider; @Programmatic public DeploymentCategoryProvider getDeploymentCategoryProvider() { return deploymentCategoryProvider != null ? deploymentCategoryProvider : (deploymentCategoryProvider = lookupServiceElseFail(DeploymentCategoryProvider.class)); } //endregion }