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 javax.inject.Inject; 028import javax.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("dependency"); 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 beacause proxies for the same 068 * dependency will not be serialized twice. 069 * 070 * @see LazyInitProxyFactory 071 * @see SpringBean 072 * @see SpringBeanLocator 073 * @see javax.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<?>, Class<?>>, 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 Class<?> generic = ResolvableType.forField(field).resolveGeneric(0); 135 String beanName = getBeanName(field, name, required, generic); 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, Class<?> generic) 189 { 190 if (Strings.isEmpty(name)) 191 { 192 Class<?> fieldType = field.getType(); 193 194 SimpleEntry<Class<?>, Class<?>> key = new SimpleEntry<Class<?>, Class<?>>(fieldType, generic); 195 name = beanNameCache.get(key); 196 if (name == null) 197 { 198 name = getBeanNameOfClass(contextLocator.getSpringContext(), fieldType, generic, 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 Class<?> generic, String fieldName) 227 { 228 // get the list of all possible matching beans 229 List<String> names = new ArrayList<>( 230 Arrays.asList(BeanFactoryUtils.beanNamesForTypeIncludingAncestors(ctx, clazz))); 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 if (generic != null) 282 { 283 return null; 284 } 285 286 StringBuilder msg = new StringBuilder(); 287 msg.append("More than one bean of type ["); 288 msg.append(clazz.getName()); 289 msg.append("] found, you have to specify the name of the bean "); 290 msg.append("(@SpringBean(name=\"foo\")) or (@Named(\"foo\") if using @javax.inject classes) in order to resolve this conflict. "); 291 msg.append("Matched beans: "); 292 msg.append(Strings.join(",", names)); 293 throw new IllegalStateException(msg.toString()); 294 } 295 else if(!names.isEmpty()) 296 { 297 return names.get(0); 298 } 299 300 return null; 301 } 302 303 public BeanDefinition getBeanDefinition(final ConfigurableListableBeanFactory beanFactory, 304 final String name) 305 { 306 if (beanFactory.containsBeanDefinition(name)) 307 { 308 return beanFactory.getBeanDefinition(name); 309 } 310 else 311 { 312 BeanFactory parent = beanFactory.getParentBeanFactory(); 313 if ((parent != null) && (parent instanceof ConfigurableListableBeanFactory)) 314 { 315 return getBeanDefinition((ConfigurableListableBeanFactory)parent, name); 316 } 317 else 318 { 319 return null; 320 } 321 } 322 } 323 324 @Override 325 public boolean supportsField(final Field field) 326 { 327 return field.isAnnotationPresent(SpringBean.class) || field.isAnnotationPresent(Inject.class); 328 } 329}