001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 *
010 *        http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License.
018 */
019
020package org.apache.isis.core.metamodel.services;
021
022import static org.apache.isis.core.commons.ensure.Ensure.ensureThatArg;
023import static org.hamcrest.CoreMatchers.is;
024import static org.hamcrest.CoreMatchers.not;
025import static org.hamcrest.CoreMatchers.nullValue;
026
027import java.lang.reflect.Field;
028import java.lang.reflect.InvocationTargetException;
029import java.lang.reflect.Method;
030import java.util.Arrays;
031import java.util.Collections;
032import java.util.List;
033import java.util.Map;
034
035import javax.inject.Inject;
036
037import com.google.common.base.Predicate;
038import com.google.common.collect.Iterables;
039import com.google.common.collect.Lists;
040import com.google.common.collect.Maps;
041
042import org.slf4j.Logger;
043import org.slf4j.LoggerFactory;
044
045import org.apache.isis.applib.DomainObjectContainer;
046import org.apache.isis.core.commons.ensure.Assert;
047import org.apache.isis.core.commons.lang.ObjectExtensions;
048import org.apache.isis.core.commons.util.ToString;
049import org.apache.isis.core.metamodel.exceptions.MetaModelException;
050import org.apache.isis.core.metamodel.runtimecontext.ServicesInjectorAware;
051
052/**
053 * Must be a thread-safe.
054 */
055public class ServicesInjectorDefault implements ServicesInjectorSpi {
056
057    private static final Logger LOG = LoggerFactory.getLogger(ServicesInjectorDefault.class);
058
059    /**
060     * If no key, not yet searched for type; otherwise the {@link List} indicates
061     * whether a service was found.
062     */
063    private final Map<Class<?>, List<Object>> servicesByType = Maps.newHashMap();
064
065    private DomainObjectContainer container;
066    private final List<Object> services = Lists.newArrayList();
067
068    
069    // /////////////////////////////////////////////////////////
070    // Constructor, init, shutdown
071    // /////////////////////////////////////////////////////////
072
073
074    @Override
075    public void init() {
076        autowireServicesAndContainer();
077    }
078
079    @Override
080    public void shutdown() {
081    }
082
083    // /////////////////////////////////////////////////////////
084    // Container
085    // /////////////////////////////////////////////////////////
086
087    @Override
088    public DomainObjectContainer getContainer() {
089        return container;
090    }
091
092    @Override
093    public void setContainer(final DomainObjectContainer container) {
094        ensureThatArg(container, is(not(nullValue())));
095        this.container = container;
096        autowireServicesAndContainer();
097    }
098
099    // /////////////////////////////////////////////////////////
100    // Services
101    // /////////////////////////////////////////////////////////
102
103    @Override
104    public void setServices(final List<Object> services) {
105        this.services.clear();
106        addServices(services);
107        autowireServicesAndContainer();
108    }
109
110    @Override
111    public List<Object> getRegisteredServices() {
112        return Collections.unmodifiableList(services);
113    }
114
115    private void addServices(final List<Object> services) {
116        for (final Object service : services) {
117            if (service instanceof List) {
118                final List<Object> serviceList = ObjectExtensions.asListT(service, Object.class);
119                addServices(serviceList);
120            } else {
121                addService(service);
122            }
123        }
124    }
125
126    private boolean addService(final Object service) {
127        return services.add(service);
128    }
129
130    // /////////////////////////////////////////////////////////
131    // Inject Dependencies
132    // /////////////////////////////////////////////////////////
133
134    @Override
135    public void injectServicesInto(final Object object) {
136        Assert.assertNotNull("no container", container);
137        Assert.assertNotNull("no services", services);
138
139        final List<Object> servicesCopy = Lists.newArrayList(services);
140        servicesCopy.add(container);
141        injectServices(object, servicesCopy);
142    }
143
144    @Override
145    public void injectServicesInto(final List<Object> objects) {
146        for (final Object object : objects) {
147            injectServicesInto(object);
148        }
149    }
150
151    // ////////////////////////////////////////////////////////////////////
152    // injectInto
153    // ////////////////////////////////////////////////////////////////////
154
155    /**
156     * That is, injecting this injector...
157     */
158    @Override
159    public void injectInto(final Object candidate) {
160        if (ServicesInjectorAware.class.isAssignableFrom(candidate.getClass())) {
161            final ServicesInjectorAware cast = ServicesInjectorAware.class.cast(candidate);
162            cast.setServicesInjector(this);
163        }
164    }
165
166
167    // /////////////////////////////////////////////////////////
168    // Helpers
169    // /////////////////////////////////////////////////////////
170
171    private static void injectServices(final Object object, final List<Object> services) {
172        final Class<?> cls = object.getClass();
173
174        autowireViaFields(object, services, cls);
175        autowireViaPrefixedMethods(object, services, cls, "set");
176        autowireViaPrefixedMethods(object, services, cls, "inject");
177    }
178
179    private static void autowireViaFields(final Object object, final List<Object> services, final Class<?> cls) {
180        final List<Field> fields = Arrays.asList(cls.getDeclaredFields());
181        final Iterable<Field> injectFields = Iterables.filter(fields, new Predicate<Field>() {
182            @Override
183            public boolean apply(Field input) {
184                final Inject annotation = input.getAnnotation(javax.inject.Inject.class);
185                return annotation != null;
186            }
187        });
188
189        for (final Field field : injectFields) {
190            for (final Object service : services) {
191                final Class<?> serviceClass = service.getClass();
192                boolean isInjectorField = isInjectorFieldFor(field, serviceClass);
193                if(isInjectorField) {
194                    field.setAccessible(true);
195                    invokeInjectorField(field, object, service);
196                }
197            }
198        }
199        
200        // recurse up the hierarchy
201        final Class<?> superclass = cls.getSuperclass();
202        if(superclass != null) {
203            autowireViaFields(object, services, superclass);
204        }
205    }
206    
207    private static void autowireViaPrefixedMethods(final Object object, final List<Object> services, final Class<?> cls, final String prefix) {
208        final List<Method> methods = Arrays.asList(cls.getMethods());
209        final Iterable<Method> prefixedMethods = Iterables.filter(methods, new Predicate<Method>(){
210            public boolean apply(Method method) {
211                final String methodName = method.getName();
212                return methodName.startsWith(prefix);
213            }
214        });
215        
216        for (final Method prefixedMethod : prefixedMethods) {
217            for (final Object service : services) {
218                final Class<?> serviceClass = service.getClass();
219                boolean isInjectorMethod = isInjectorMethodFor(prefixedMethod, serviceClass);
220                if(isInjectorMethod) {
221                    prefixedMethod.setAccessible(true);
222                    invokeInjectorMethod(prefixedMethod, object, service);
223                }
224            }
225        }
226    }
227
228    private static boolean isInjectorFieldFor(final Field field, final Class<?> serviceClass) {
229        final Class<?> type = field.getType();
230        // don't think that type can ever be null, but Javadoc for java.lang.reflect.Field doesn't say
231        return type != null && type.isAssignableFrom(serviceClass);
232    }
233
234    public static boolean isInjectorMethodFor(Method method, final Class<?> serviceClass) {
235        final String methodName = method.getName();
236        if (methodName.startsWith("set") || methodName.startsWith("inject")) {
237            final Class<?>[] parameterTypes = method.getParameterTypes();
238            if (parameterTypes.length == 1 && parameterTypes[0] != Object.class && parameterTypes[0].isAssignableFrom(serviceClass)) {
239                return true;
240            }
241        }
242        return false;
243    }
244    
245    private static void invokeMethod(final Method method, final Object target, final Object[] parameters) {
246        try {
247            method.invoke(target, parameters);
248        } catch (final SecurityException e) {
249            throw new MetaModelException(String.format("Cannot access the %s method in %s", method.getName(), target.getClass().getName()));
250        } catch (final IllegalArgumentException e1) {
251            throw new MetaModelException(e1);
252        } catch (final IllegalAccessException e1) {
253            throw new MetaModelException(String.format("Cannot access the %s method in %s", method.getName(), target.getClass().getName()));
254        } catch (final InvocationTargetException e) {
255            final Throwable targetException = e.getTargetException();
256            if (targetException instanceof RuntimeException) {
257                throw (RuntimeException) targetException;
258            } else {
259                throw new MetaModelException(targetException);
260            }
261        }
262    }
263
264    private static void invokeInjectorField(final Field field, final Object target, final Object parameter) {
265        try {
266            field.set(target, parameter);
267        } catch (IllegalArgumentException e) {
268            throw new MetaModelException(e);
269        } catch (IllegalAccessException e) {
270            throw new MetaModelException(String.format("Cannot access the %s field in %s", field.getName(), target.getClass().getName()));
271        }
272        if (LOG.isDebugEnabled()) {
273            LOG.debug("injected " + parameter + " into " + new ToString(target));
274        }
275    }
276
277    private static void invokeInjectorMethod(final Method method, final Object target, final Object parameter) {
278        final Object[] parameters = new Object[] { parameter };
279        invokeMethod(method, target, parameters);
280        if (LOG.isDebugEnabled()) {
281            LOG.debug("injected " + parameter + " into " + new ToString(target));
282        }
283    }
284    
285    private void autowireServicesAndContainer() {
286        injectServicesInto(this.services);
287        injectServicesInto(this.container);
288    }
289
290    @Override
291    public <T> T lookupService(Class<T> serviceClass) {
292        List<T> services = lookupServices(serviceClass);
293        return !services.isEmpty() ? services.get(0) : null;
294    }
295
296    @SuppressWarnings("unchecked")
297    @Override
298    public <T> List<T> lookupServices(Class<T> serviceClass) {
299        locateAndCache(serviceClass);
300        return (List<T>) servicesByType.get(serviceClass);
301    };
302
303    private void locateAndCache(Class<?> serviceClass) {
304        if(servicesByType.containsKey(serviceClass)) {
305           return; 
306        }
307
308        List<Object> matchingServices = Lists.newArrayList();
309        addAssignableTo(serviceClass, services, matchingServices);
310        addAssignableTo(serviceClass, singletonListFor(container), matchingServices);
311        
312        servicesByType.put(serviceClass, matchingServices);
313    }
314
315    private static List<Object> singletonListFor(Object obj) {
316        return obj!=null? Collections.singletonList(obj): Collections.emptyList();
317    }
318
319    private static void addAssignableTo(Class<?> type, List<Object> candidates, List<Object> filteredServicesAndContainer) {
320        Iterable<Object> filteredServices = Iterables.filter(candidates, ofType(type));
321        filteredServicesAndContainer.addAll(Lists.newArrayList(filteredServices));
322    }
323
324    private static final Predicate<Object> ofType(final Class<?> cls) {
325        return new Predicate<Object>() {
326            @Override
327            public boolean apply(Object input) {
328                return cls.isAssignableFrom(input.getClass());
329            }
330        };
331    }
332
333}