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.injection.annot;
018
019import java.lang.reflect.Field;
020import java.util.AbstractMap.SimpleEntry;
021import java.util.ArrayList;
022import java.util.Arrays;
023import java.util.Iterator;
024import java.util.List;
025import java.util.concurrent.ConcurrentMap;
026
027import jakarta.inject.Inject;
028import jakarta.inject.Named;
029
030import org.apache.wicket.injection.IFieldValueFactory;
031import org.apache.wicket.proxy.LazyInitProxyFactory;
032import org.apache.wicket.spring.ISpringContextLocator;
033import org.apache.wicket.spring.SpringBeanLocator;
034import org.apache.wicket.util.lang.Args;
035import org.apache.wicket.util.lang.Generics;
036import org.apache.wicket.util.string.Strings;
037import org.springframework.beans.factory.BeanFactory;
038import org.springframework.beans.factory.BeanFactoryUtils;
039import org.springframework.beans.factory.config.BeanDefinition;
040import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
041import org.springframework.beans.factory.support.AbstractBeanDefinition;
042import org.springframework.context.ApplicationContext;
043import org.springframework.context.support.AbstractApplicationContext;
044import org.springframework.core.ResolvableType;
045
046/**
047 * {@link IFieldValueFactory} that uses {@link LazyInitProxyFactory} to create proxies for Spring
048 * dependencies based on the {@link SpringBean} annotation applied to a field. This class is usually
049 * used by the {@link SpringComponentInjector} to inject objects with lazy init proxies. However,
050 * this class can be used on its own to create proxies for any field decorated with a
051 * {@link SpringBean} annotation.
052 * <p>
053 * Example:
054 * 
055 * <pre>
056 * IFieldValueFactory factory = new AnnotProxyFieldValueFactory(contextLocator);
057 * field = obj.getClass().getDeclaredField(&quot;dependency&quot;);
058 * IDependency dependency = (IDependency)factory.getFieldValue(field, obj);
059 * </pre>
060 * 
061 * In the example above the
062 * 
063 * <code>dependency</code> object returned is a lazy init proxy that will look up the actual
064 * IDependency bean from spring context upon first access to one of the methods.
065 * <p>
066 * This class will also cache any produced proxies so that the same proxy is always returned for the
067 * same spring dependency. This helps cut down on session size because proxies for the same
068 * dependency will not be serialized twice.
069 * 
070 * @see LazyInitProxyFactory
071 * @see SpringBean
072 * @see SpringBeanLocator
073 * @see jakarta.inject.Inject
074 * 
075 * @author Igor Vaynberg (ivaynberg)
076 * @author Istvan Devai
077 * @author Tobias Soloschenko
078 */
079public class AnnotProxyFieldValueFactory implements IFieldValueFactory
080{
081        private final ISpringContextLocator contextLocator;
082
083        private final ConcurrentMap<SpringBeanLocator, Object> cache = Generics.newConcurrentHashMap();
084
085        private final ConcurrentMap<SimpleEntry<Class<?>, ResolvableType>,
086                                                                String> beanNameCache = Generics.newConcurrentHashMap();
087
088        private final boolean wrapInProxies;
089
090        /**
091         * @param contextLocator
092         *            spring context locator
093         */
094        public AnnotProxyFieldValueFactory(final ISpringContextLocator contextLocator)
095        {
096                this(contextLocator, true);
097        }
098
099        /**
100         * @param contextLocator
101         *            spring context locator
102         * @param wrapInProxies
103         *            whether or not wicket should wrap dependencies with specialized proxies that can
104         *            be safely serialized. in most cases this should be set to true.
105         */
106        public AnnotProxyFieldValueFactory(final ISpringContextLocator contextLocator,
107                final boolean wrapInProxies)
108        {
109                this.contextLocator = Args.notNull(contextLocator, "contextLocator");
110                this.wrapInProxies = wrapInProxies;
111        }
112
113        @Override
114        public Object getFieldValue(final Field field, final Object fieldOwner)
115        {
116                if (supportsField(field))
117                {
118                        SpringBean annot = field.getAnnotation(SpringBean.class);
119
120                        String name;
121                        boolean required;
122                        if (annot != null)
123                        {
124                                name = annot.name();
125                                required = annot.required();
126                        }
127                        else
128                        {
129                                Named named = field.getAnnotation(Named.class);
130                                name = named != null ? named.value() : "";
131                                required = true;
132                        }
133
134                        ResolvableType resolvableType = ResolvableType.forField(field);
135                        String beanName = getBeanName(field, name, required, resolvableType);
136
137                        SpringBeanLocator locator = new SpringBeanLocator(beanName, field.getType(), field, contextLocator);
138
139                        // only check the cache if the bean is a singleton
140                        Object cachedValue = cache.get(locator);
141                        if (cachedValue != null)
142                        {
143                                return cachedValue;
144                        }
145
146                        Object target;
147                        try
148                        {
149                                // check whether there is a bean with the provided properties
150                                target = locator.locateProxyTarget();
151                        }
152                        catch (IllegalStateException isx)
153                        {
154                                if (required)
155                                {
156                                        throw isx;
157                                }
158                                else
159                                {
160                                        return null;
161                                }
162                        }
163
164                        if (wrapInProxies)
165                        {
166                                target = LazyInitProxyFactory.createProxy(field.getType(), locator);
167                        }
168
169                        // only put the proxy into the cache if the bean is a singleton
170                        if (locator.isSingletonBean())
171                        {
172                                Object tmpTarget = cache.putIfAbsent(locator, target);
173                                if (tmpTarget != null)
174                                {
175                                        target = tmpTarget;
176                                }
177                        }
178                        return target;
179                }
180                return null;
181        }
182
183        /**
184         * 
185         * @param field
186         * @return bean name
187         */
188        private String getBeanName(final Field field, String name, boolean required, ResolvableType resolvableType)
189        {
190                if (Strings.isEmpty(name))
191                {
192                        Class<?> fieldType = field.getType();
193                        
194                        SimpleEntry<Class<?>, ResolvableType> key = new SimpleEntry<>(fieldType, resolvableType);
195                        name = beanNameCache.get(key);
196                        if (name == null)
197                        {
198                                name = getBeanNameOfClass(contextLocator.getSpringContext(), fieldType, resolvableType, field.getName());
199                                if (name != null)
200                                {
201                                        String tmpName = beanNameCache.putIfAbsent(key, name);
202                                        if (tmpName != null)
203                                        {
204                                                name = tmpName;
205                                        }
206                                }
207                        }
208                }
209
210                return name;
211        }
212
213        /**
214         * Returns the name of the Bean as registered to Spring. Throws IllegalState exception if none
215         * or more than one beans are found.
216         * 
217         * @param ctx
218         *            spring application context
219         * @param clazz
220         *            bean class
221         * @param fieldName 
222         * @throws IllegalStateException
223         * @return spring name of the bean
224         */
225        private String getBeanNameOfClass(final ApplicationContext ctx, final Class<?> clazz,
226                final ResolvableType resolvableType, String fieldName)
227        {
228                // get the list of all possible matching beans
229                List<String> names = new ArrayList<>(
230                        Arrays.asList(BeanFactoryUtils.beanNamesForTypeIncludingAncestors(ctx, resolvableType)));
231
232                // filter out beans that are not candidates for autowiring
233                if (ctx instanceof AbstractApplicationContext)
234                {
235                        Iterator<String> it = names.iterator();
236                        while (it.hasNext())
237                        {
238                                final String possibility = it.next();
239                                BeanDefinition beanDef = getBeanDefinition(
240                                        ((AbstractApplicationContext)ctx).getBeanFactory(), possibility);
241                                if (BeanFactoryUtils.isFactoryDereference(possibility) ||
242                                        possibility.startsWith("scopedTarget.") ||
243                                        (beanDef != null && !beanDef.isAutowireCandidate()))
244                                {
245                                        it.remove();
246                                }
247                        }
248                }
249
250                if (names.size() > 1)
251                {
252                        if (ctx instanceof AbstractApplicationContext)
253                        {
254                                List<String> primaries = new ArrayList<>();
255                                for (String name : names)
256                                {
257                                        BeanDefinition beanDef = getBeanDefinition(
258                                                ((AbstractApplicationContext)ctx).getBeanFactory(), name);
259                                        if (beanDef instanceof AbstractBeanDefinition)
260                                        {
261                                                if (beanDef.isPrimary())
262                                                {
263                                                        primaries.add(name);
264                                                }
265                                        }
266                                }
267                                if (primaries.size() == 1)
268                                {
269                                        return primaries.get(0);
270                                }
271                        }
272                        
273                        //use field name to find a match
274                        int nameIndex = names.indexOf(fieldName);
275                        
276                        if (nameIndex > -1)
277                        {
278                                return names.get(nameIndex);
279                        }
280
281                        StringBuilder msg = new StringBuilder();
282                        msg.append("More than one bean of type [");
283                        msg.append(clazz.getName());
284                        msg.append("] found, you have to specify the name of the bean ");
285                        msg.append("(@SpringBean(name=\"foo\")) or (@Named(\"foo\") if using @jakarta.inject classes) in order to resolve this conflict. ");
286                        msg.append("Matched beans: ");
287                        msg.append(Strings.join(",", names));
288                        throw new IllegalStateException(msg.toString());
289                }
290                else if(!names.isEmpty())
291                {
292                        return names.get(0);
293                }
294                
295                return null;
296        }
297
298        public BeanDefinition getBeanDefinition(final ConfigurableListableBeanFactory beanFactory,
299                final String name)
300        {
301                if (beanFactory.containsBeanDefinition(name))
302                {
303                        return beanFactory.getBeanDefinition(name);
304                }
305                else
306                {
307                        BeanFactory parent = beanFactory.getParentBeanFactory();
308                        if ((parent != null) && (parent instanceof ConfigurableListableBeanFactory))
309                        {
310                                return getBeanDefinition((ConfigurableListableBeanFactory)parent, name);
311                        }
312                        else
313                        {
314                                return null;
315                        }
316                }
317        }
318
319        @Override
320        public boolean supportsField(final Field field)
321        {
322                return field.isAnnotationPresent(SpringBean.class) || field.isAnnotationPresent(Inject.class);
323        }
324}