001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.spring;
018
019import java.lang.ref.WeakReference;
020import java.lang.reflect.Field;
021import java.util.ArrayList;
022import java.util.Arrays;
023import java.util.Iterator;
024import java.util.List;
025import java.util.Map;
026import java.util.Set;
027
028import org.apache.wicket.core.util.lang.WicketObjects;
029import org.apache.wicket.proxy.IProxyTargetLocator;
030import org.apache.wicket.util.lang.Args;
031import org.apache.wicket.util.lang.Objects;
032import org.springframework.beans.factory.NoSuchBeanDefinitionException;
033import org.springframework.beans.factory.config.BeanDefinition;
034import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
035import org.springframework.beans.factory.support.RootBeanDefinition;
036import org.springframework.context.ApplicationContext;
037import org.springframework.context.support.AbstractApplicationContext;
038import org.springframework.core.ResolvableType;
039
040/**
041 * Implementation of {@link IProxyTargetLocator} that can locate beans within a spring application
042 * context. Beans are looked up by the combination of name and type, if name is omitted only type is
043 * used.
044 * 
045 * @author Igor Vaynberg (ivaynberg)
046 * @author Istvan Devai
047 * @author Tobias Soloschenko
048 */
049public class SpringBeanLocator implements IProxyTargetLocator
050{
051        private static final long serialVersionUID = 1L;
052
053        // Weak reference so we don't hold up WebApp classloader garbage collection.
054        private transient WeakReference<Class<?>> beanTypeCache;
055
056        private final String beanTypeName;
057
058        private String beanName;
059
060        private ISpringContextLocator springContextLocator;
061
062        private Boolean singletonCache = null;
063
064        /**
065         * Resolvable type for field to inject
066         */
067        private ResolvableType fieldResolvableType;
068
069        /**
070         * If the field to inject is a list this is the resolvable type of its elements
071         */
072        private ResolvableType fieldElementsResolvableType;
073
074        /**
075         * Constructor
076         * 
077         * @param beanType
078         *            bean class
079         * @param locator
080         *            spring context locator
081         */
082        public SpringBeanLocator(final Class<?> beanType, final ISpringContextLocator locator)
083        {
084                this(null, beanType, null, locator);
085        }
086
087        public SpringBeanLocator(final String beanName, final Class<?> beanType,
088                final ISpringContextLocator locator)
089        {
090                this(beanName, beanType, null, locator);
091        }
092
093        /**
094         * Constructor
095         * 
096         * @param beanType
097         *            bean class
098         * @param locator
099         *            spring context locator
100         */
101        public SpringBeanLocator(final Class<?> beanType, Field beanField,
102                final ISpringContextLocator locator)
103        {
104                this(null, beanType, beanField, locator);
105        }
106
107        /**
108         * Constructor
109         * 
110         * @param beanName
111         *            bean name
112         * @param beanType
113         *            bean class
114         * @param locator
115         *            spring context locator
116         */
117        public SpringBeanLocator(final String beanName, final Class<?> beanType, Field beanField,
118                final ISpringContextLocator locator)
119        {
120                Args.notNull(locator, "locator");
121                Args.notNull(beanType, "beanType");
122
123                this.beanName = beanName;
124                beanTypeCache = new WeakReference<Class<?>>(beanType);
125                beanTypeName = beanType.getName();
126                springContextLocator = locator;
127                
128                if (beanField != null)
129                {
130                        fieldResolvableType = ResolvableType.forField(beanField);
131                        fieldElementsResolvableType = extractElementGeneric(fieldResolvableType);
132                }
133        }
134        
135        /**
136         * If the field type is a collection (Map, Set or List) extracts type 
137         * information about its elements.
138         * 
139         * @param fieldResolvableType
140         *                              the resolvable type of the field
141         * @return the resolvable type of elements of the field, if any.
142         */
143        private ResolvableType extractElementGeneric(ResolvableType fieldResolvableType)
144        {
145                Class<?> clazz = fieldResolvableType.resolve();
146                
147                if (Set.class.isAssignableFrom(clazz) || List.class.isAssignableFrom(clazz))
148                {
149                        return fieldResolvableType.getGeneric();
150                } 
151                else if (Map.class.isAssignableFrom(clazz))
152                {
153                        return fieldResolvableType.getGeneric(1);
154                }
155                
156                return null;
157        }
158
159        /**
160         * @return returns whether the bean (the locator is supposed to istantiate) is a singleton or
161         *         not
162         */
163        public boolean isSingletonBean()
164        {
165                if (singletonCache == null)
166                {
167                        singletonCache = getBeanName() != null && 
168                                getSpringContext().isSingleton(getBeanName());
169                }
170                return singletonCache;
171        }
172
173        /**
174         * @return bean class this locator is configured with
175         */
176        public Class<?> getBeanType()
177        {
178                Class<?> clazz = beanTypeCache == null ? null : beanTypeCache.get();
179                if (clazz == null)
180                {
181                        beanTypeCache = new WeakReference<>(
182                                clazz = WicketObjects.resolveClass(beanTypeName));
183                        if (clazz == null)
184                        {
185                                throw new RuntimeException("SpringBeanLocator could not find class [" +
186                                        beanTypeName + "] needed to locate the [" +
187                                        ((beanName != null) ? (beanName) : ("bean name not specified")) + "] bean");
188                        }
189                }
190                return clazz;
191        }
192
193        @Override
194        public Object locateProxyTarget()
195        {
196                final ApplicationContext context = getSpringContext();
197
198                return lookupSpringBean(context, beanName, getBeanType());
199        }
200
201        /**
202         * 
203         * @return ApplicationContext
204         */
205        private ApplicationContext getSpringContext()
206        {
207                final ApplicationContext context = springContextLocator.getSpringContext();
208
209                if (context == null)
210                {
211                        throw new IllegalStateException("spring application context locator returned null");
212                }
213                return context;
214        }
215
216        /**
217         * @return bean name this locator is configured with
218         */
219        public final String getBeanName()
220        {
221                return beanName;
222        }
223
224        /**
225         * @return context locator this locator is configured with
226         */
227        public final ISpringContextLocator getSpringContextLocator()
228        {
229                return springContextLocator;
230        }
231
232        /**
233         * Looks up a bean by its name and class. Throws IllegalState exception if bean not found.
234         * 
235         * @param ctx
236         *            spring application context
237         * 
238         * @param name
239         *            bean name
240         * @param clazz
241         *            bean class
242         * @throws java.lang.IllegalStateException
243         * @return found bean
244         */
245        private Object lookupSpringBean(ApplicationContext ctx, String name, Class<?> clazz)
246        {
247                try
248                {
249                        // If the name is set the lookup is clear
250                        if (name != null)
251                        {
252                                return ctx.getBean(name, clazz);
253                        }
254
255                        // If the beanField information is null the clazz is going to be used
256                        if (fieldResolvableType == null)
257                        {
258                                return ctx.getBean(clazz);
259                        }
260
261                        // If the given class is a list try to get the generic of the list
262                        Class<?> lookupClass = fieldElementsResolvableType != null ? 
263                                fieldElementsResolvableType.resolve() : clazz;
264
265                        // Else the lookup is done via Generic
266                        List<String> names = loadBeanNames(ctx, lookupClass);
267
268                        Object foundBeans = getBeansByName(ctx, names);
269
270                        if(foundBeans != null)
271                        {
272                                return foundBeans;
273                        }
274
275                        throw new IllegalStateException(
276                                "Concrete bean could not be received from the application context for class: " +
277                                        clazz.getName() + ".");
278                }
279                catch (NoSuchBeanDefinitionException e)
280                {
281                        throw new IllegalStateException("bean with name [" + name + "] and class [" +
282                                clazz.getName() + "] not found", e);
283                }
284        }
285
286        /**
287         * Returns a list of candidate names for the given class.
288         * 
289         * @param ctx
290         *                      spring application context
291         * @param lookupClass
292         *                      the class to lookup
293         * @return a list of candidate names
294         */
295        private List<String> loadBeanNames(ApplicationContext ctx, Class<?> lookupClass)
296        {               
297                List<String> beanNames = new ArrayList<>();
298                Class<?> fieldType = getBeanType();
299                String[] beanNamesArr = ctx.getBeanNamesForType(fieldType);
300                
301                //add names for field class 
302                beanNames.addAll(Arrays.asList(beanNamesArr));
303                
304                //add names for lookup class 
305                if (lookupClass != fieldType)
306                {
307                        beanNamesArr = ctx.getBeanNamesForType(lookupClass);
308                        beanNames.addAll(Arrays.asList(beanNamesArr));
309                }
310                
311                Iterator<String> nameIterator = beanNames.iterator();
312                
313                //filter those beans who don't have a definition (used internally by Spring)
314                while (nameIterator.hasNext())
315                {
316                        if (!ctx.containsBeanDefinition(nameIterator.next()))
317                        {
318                                nameIterator.remove();
319                        }                       
320                }
321                
322                return beanNames;
323        }
324
325        /**
326         * Retrieves a list of beans or a single bean for the given list of names and assignable to the
327         * current field to inject.
328         * 
329         * @param ctx
330         *                              spring application context.
331         * @param names
332         *                              the list of candidate names
333         * @return a list of matching beans or a single one.
334         */
335        private Object getBeansByName(ApplicationContext ctx, List<String> names)
336        {
337                FieldBeansCollector beansCollector = new FieldBeansCollector(fieldResolvableType);
338                
339                for (String beanName : names)
340                {
341                        RootBeanDefinition beanDef = getBeanDefinition(ctx, beanName);
342
343                        if (beanDef == null)
344                        {
345                                continue;
346                        }
347
348                        ResolvableType candidateResolvableType = null;
349
350                        //check if we have the class of the bean or the factory method.
351                        //Usually if use XML as config file we have the class while we 
352                        //have the factory method if we use Java-based configuration.
353                        if (beanDef.hasBeanClass())
354                        {
355                                candidateResolvableType = ResolvableType.forClass(beanDef.getBeanClass());
356                        }
357                        else if (beanDef.getResolvedFactoryMethod() != null)
358                        {
359                                candidateResolvableType = ResolvableType.forMethodReturnType(
360                                        beanDef.getResolvedFactoryMethod());
361                        }
362
363                        if (candidateResolvableType == null)
364                        {
365                                continue;
366                        }
367
368                        boolean exactMatch = fieldResolvableType.isAssignableFrom(candidateResolvableType);
369                        boolean elementMatch = fieldElementsResolvableType != null && fieldElementsResolvableType.isAssignableFrom(candidateResolvableType);
370
371                        if (exactMatch)
372                        {
373                                this.beanName = beanName;
374                                return ctx.getBean(beanName);
375                        }
376                        
377                        if (elementMatch)
378                        {
379                                beansCollector.addBean(beanName, ctx.getBean(beanName));
380                        }
381
382                }
383                
384                return beansCollector.getBeansToInject();
385        }
386
387        @Override
388        public boolean equals(final Object obj)
389        {
390                if (obj instanceof SpringBeanLocator)
391                {
392                        SpringBeanLocator other = (SpringBeanLocator)obj;
393                        return beanTypeName.equals(other.beanTypeName) &&
394                                Objects.equal(beanName, other.beanName);
395                }
396                return false;
397        }
398
399        @Override
400        public int hashCode()
401        {
402                int hashcode = beanTypeName.hashCode();
403                if (getBeanName() != null)
404                {
405                        hashcode = hashcode + (127 * beanName.hashCode());
406                }
407                return hashcode;
408        }
409
410        /**
411         * Gets the root bean definition for the given name.
412         * 
413         * @param ctx
414         *                              spring application context.
415         * @param name
416         *                              bean name
417         * @return bean definition for the current name, null if such a definition is not found.
418         */
419        public RootBeanDefinition getBeanDefinition(final ApplicationContext ctx, final String name)
420        {
421                ConfigurableListableBeanFactory beanFactory = ((AbstractApplicationContext)ctx).getBeanFactory();
422
423                BeanDefinition beanDef = beanFactory.containsBean(name) ?
424                        beanFactory.getMergedBeanDefinition(name) : null;
425
426                if (beanDef instanceof RootBeanDefinition) 
427                {
428                        return (RootBeanDefinition)beanDef;
429                }
430
431                return null;
432        }
433}