org.apache.isis.core.metamodel.services.ServicesInjectorDefault.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.isis.core.metamodel.services.ServicesInjectorDefault.java

Source

/*
 *  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.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
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.core.commons.ensure.Assert;
import org.apache.isis.core.commons.lang.ObjectExtensions;
import org.apache.isis.core.commons.util.ToString;
import org.apache.isis.core.metamodel.exceptions.MetaModelException;
import org.apache.isis.core.metamodel.runtimecontext.ServicesInjectorAware;
import org.apache.isis.core.metamodel.spec.InjectorMethodEvaluator;
import org.apache.isis.core.metamodel.spec.SpecificationLoader;
import org.apache.isis.core.metamodel.spec.SpecificationLoaderAware;
import org.apache.isis.core.metamodel.specloader.InjectorMethodEvaluatorDefault;
import org.apache.isis.core.metamodel.specloader.ServiceInitializer;

/**
 * Must be a thread-safe.
 */
public class ServicesInjectorDefault implements ServicesInjectorSpi, SpecificationLoaderAware {

    private static final Logger LOG = LoggerFactory.getLogger(ServicesInjectorDefault.class);

    /**
     * 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();

    public ServicesInjectorDefault(final List<Object> services) {
        this(services, null);
    }

    /**
     * For testing.  
     * 
     * <p>
     *     In production code, {@link #setSpecificationLoader(org.apache.isis.core.metamodel.spec.SpecificationLoader)}
     *     Pis used instead.
     * </p>
     * @param injectorMethodEvaluator
     */
    public ServicesInjectorDefault(final List<Object> services,
            final InjectorMethodEvaluator injectorMethodEvaluator) {
        this.services.addAll(services);
        this.injectorMethodEvaluator = injectorMethodEvaluator != null ? injectorMethodEvaluator
                : new InjectorMethodEvaluatorDefault();

        autowireServicesAndContainer();
    }

    //region > init, shutdown

    @Override
    public void init() {
        autowireServicesAndContainer();
    }

    @Override
    public void shutdown() {
    }

    //endregion

    //region > replaceServices

    @Override
    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();

        autowireServicesAndContainer();
    }

    @Override
    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);
    }

    @Override
    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, and that the {@link PostConstruct} method, if present, must either
     * take no arguments or take a {@link Map} object), and that the {@link PreDestroy} method, if present, must take
     * no arguments.
     *
     * <p>
     * TODO: there seems to be some duplication/overlap with {@link ServiceInitializer}.
     */
    @Override
    public void validateServices() {
        validate(getRegisteredServices());
    }

    private static void validate(List<Object> serviceList) {
        for (Object service : serviceList) {
            final Method[] methods = service.getClass().getMethods();
            for (Method method : methods) {
                validatePostConstructMethods(service, method);
                validatePreDestroyMethods(service, method);
            }
        }
        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();
    }

    private static void validatePostConstructMethods(Object service, Method method) {
        final PostConstruct postConstruct = method.getAnnotation(PostConstruct.class);
        if (postConstruct == null) {
            return;
        }
        final int numParams = method.getParameterTypes().length;
        if (numParams == 0) {
            return;
        }
        if (numParams == 1 && method.getParameterTypes()[0].isAssignableFrom(Map.class)) {
            return;
        }
        throw new IllegalStateException(
                "Domain service " + service.getClass().getName() + " has @PostConstruct method " + method.getName()
                        + "; such methods must take either no argument or 1 argument of type Map<String,String>");
    }

    private static void validatePreDestroyMethods(Object service, Method method) {
        final PreDestroy preDestroy = method.getAnnotation(PreDestroy.class);
        if (preDestroy == null) {
            return;
        }
        final int numParams = method.getParameterTypes().length;
        if (numParams == 0) {
            return;
        }
        throw new IllegalStateException("Domain service " + service.getClass().getName()
                + " has @PreDestroy method " + method.getName() + "; such methods must take no arguments");
    }

    static boolean contains(final List<Object> services, final Class<?> serviceClass) {
        for (Object service : services) {
            if (serviceClass.isAssignableFrom(service.getClass())) {
                return true;
            }
        }
        return false;
    }

    @Override
    public List<Object> getRegisteredServices() {
        return Collections.unmodifiableList(services);
    }

    private void addServices(final List<Object> services) {
        for (final Object service : services) {
            if (service instanceof List) {
                final List<Object> serviceList = ObjectExtensions.asListT(service, Object.class);
                addServices(serviceList);
            } else {
                addService(service);
            }
        }
    }

    private boolean addService(final Object service) {
        return services.add(service);
    }

    //endregion

    //region > injectServicesInto

    @Override
    public void injectServicesInto(final Object object) {
        Assert.assertNotNull("no services", services);

        injectServices(object, Collections.unmodifiableList(services));
    }

    @Override
    public void injectServicesInto(final List<Object> objects) {
        for (final Object object : objects) {
            injectServicesInto(object);
        }
    }

    //endregion

    //region > injectInto

    /**
     * That is, injecting this injector...
     */
    @Override
    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);
        autowireViaPrefixedMethods(object, services, cls, "set");
        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 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) {
        for (final Object service : services) {
            final Class<?> serviceClass = service.getClass();
            final boolean canInject = isInjectorFieldFor(field, serviceClass);
            if (canInject) {
                field.setAccessible(true);
                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 boolean isInjectorFieldFor(final Field field, final Class<?> serviceClass) {
        final Class<?> type = field.getType();
        // don't think that type can ever be null, but Javadoc for java.lang.reflect.Field doesn't say
        return type != null && type.isAssignableFrom(serviceClass);
    }

    private static void invokeMethod(final Method method, final Object target, final Object[] parameters) {
        try {
            method.invoke(target, parameters);
        } catch (final SecurityException 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 IllegalAccessException e1) {
            throw new MetaModelException(String.format("Cannot access the %s method in %s", method.getName(),
                    target.getClass().getName()));
        } 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.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));
        }
    }

    private void autowireServicesAndContainer() {
        injectServicesInto(this.services);
    }

    //endregion

    //region > lookupService, lookupServices

    @Override
    public <T> T lookupService(final Class<T> serviceClass) {
        final List<T> services = lookupServices(serviceClass);
        return !services.isEmpty() ? services.get(0) : null;
    }

    @SuppressWarnings("unchecked")
    @Override
    public <T> List<T> lookupServices(final Class<T> serviceClass) {
        locateAndCache(serviceClass);
        return (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 > injected dependencies

    private InjectorMethodEvaluator injectorMethodEvaluator;

    @Override
    public void setSpecificationLoader(final SpecificationLoader specificationLookup) {
        injectorMethodEvaluator = specificationLookup;
    }

    //endregion
}