View Javadoc

1   /*
2    * Copyright 2006-2007 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.springframework.batch.core.scope.util;
17  
18  import java.beans.PropertyEditor;
19  import java.util.Date;
20  import java.util.List;
21  import java.util.Map;
22  import java.util.Set;
23  
24  import org.springframework.aop.TargetSource;
25  import org.springframework.aop.target.SimpleBeanTargetSource;
26  import org.springframework.beans.BeanWrapper;
27  import org.springframework.beans.BeanWrapperImpl;
28  import org.springframework.beans.BeansException;
29  import org.springframework.beans.PropertyEditorRegistrySupport;
30  import org.springframework.beans.TypeConverter;
31  import org.springframework.beans.TypeMismatchException;
32  import org.springframework.beans.factory.InitializingBean;
33  import org.springframework.beans.factory.config.BeanDefinition;
34  import org.springframework.beans.factory.config.BeanDefinitionHolder;
35  import org.springframework.beans.factory.config.BeanDefinitionVisitor;
36  import org.springframework.beans.factory.config.TypedStringValue;
37  import org.springframework.beans.factory.support.DefaultListableBeanFactory;
38  import org.springframework.beans.factory.support.GenericBeanDefinition;
39  import org.springframework.beans.factory.support.ManagedList;
40  import org.springframework.beans.factory.support.ManagedMap;
41  import org.springframework.beans.factory.support.ManagedSet;
42  import org.springframework.core.AttributeAccessor;
43  import org.springframework.core.MethodParameter;
44  import org.springframework.util.Assert;
45  import org.springframework.util.StringValueResolver;
46  
47  /**
48   * A {@link TargetSource} that lazily initializes its target, replacing bean
49   * definition properties dynamically if they are marked as placeholders. String
50   * values with embedded <code>%{key}</code> patterns will be replaced with the
51   * corresponding value from the injected context (which must also be a String).
52   * This includes dynamically locating a bean reference (e.g.
53   * <code>ref="%{foo}"</code>), and partial replacement of patterns (e.g.
54   * <code>value="%{foo}-bar-%{spam}"</code>). These replacements work for context
55   * values that are primitive (String, Long, Integer). You can also replace
56   * non-primitive values directly by making the whole bean property value into a
57   * placeholder (e.g. <code>value="%{foo}"</code> where <code>foo</code> is a
58   * property in the context).
59   * 
60   * @author Dave Syer
61   * 
62   */
63  public class PlaceholderTargetSource extends SimpleBeanTargetSource implements InitializingBean {
64  
65  	/**
66  	 * Key for placeholders to be replaced from the properties provided.
67  	 */
68  	private static final String PLACEHOLDER_PREFIX = "%{";
69  
70  	private static final String PLACEHOLDER_SUFFIX = "}";
71  
72  	private ContextFactory contextFactory;
73  
74  	private String beanName;
75  
76  	/**
77  	 * Public setter for the context factory. Used to construct the context root
78  	 * whenever placeholders are replaced in a bean definition.
79  	 * 
80  	 * @param contextFactory the {@link ContextFactory}
81  	 */
82  	public void setContextFactory(ContextFactory contextFactory) {
83  		this.contextFactory = contextFactory;
84  	}
85  
86  	/*
87  	 * (non-Javadoc)
88  	 * 
89  	 * @see
90  	 * org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
91  	 */
92  	public void afterPropertiesSet() {
93  		Assert.notNull(contextFactory, "The ContextFactory must be set.");
94  		beanName = getTargetBeanName() + "#" + contextFactory.getContextId();
95  	}
96  
97  	/*
98  	 * (non-Javadoc)
99  	 * 
100 	 * @see org.springframework.aop.target.LazyInitTargetSource#getTarget()
101 	 */
102 	@Override
103 	public synchronized Object getTarget() throws BeansException {
104 
105 		// Object target;
106 		Object target = getTargetFromContext();
107 		if (target != null) {
108 			return target;
109 		}
110 
111 		DefaultListableBeanFactory listableBeanFactory = (DefaultListableBeanFactory) getBeanFactory();
112 
113 		final TypeConverter typeConverter = listableBeanFactory.getTypeConverter();
114 
115 		DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(listableBeanFactory);
116 		beanFactory.copyConfigurationFrom(listableBeanFactory);
117 
118 		final TypeConverter contextTypeConverter = new TypeConverter() {
119 			@SuppressWarnings({ "unchecked", "rawtypes" })
120 			public Object convertIfNecessary(Object value, Class requiredType, MethodParameter methodParam)
121 					throws TypeMismatchException {
122 				Object result = null;
123 				if (value instanceof String) {
124 					String key = (String) value;
125 					if (key.startsWith(PLACEHOLDER_PREFIX) && key.endsWith(PLACEHOLDER_SUFFIX)) {
126 						key = extractKey(key);
127 						result = convertFromContext(key, requiredType);
128 						if (result == null) {
129 							Object property = getPropertyFromContext(key);
130 							// Give the normal type converter a chance by
131 							// reversing to a String
132 							if (property != null) {
133 								property = convertToString(property, typeConverter);
134 								if (property != null) {
135 									value = property;
136 								}
137 								logger.debug(String.format("Bound %%{%s} to String value [%s]", key, result));
138 							}
139 							else {
140 								throw new IllegalStateException("Cannot bind to placeholder: " + key);
141 							}
142 						}
143 						else {
144 							logger.debug(String.format("Bound %%{%s} to [%s]", key, result));
145 						}
146 					}
147 				}
148 				else if (requiredType.isAssignableFrom(value.getClass())) {
149 					result = value;
150 				}
151 				else if (requiredType.isAssignableFrom(String.class)) {
152 					result = convertToString(value, typeConverter);
153 					if (result == null) {
154 						logger.debug("Falling back on toString for conversion of : [" + value.getClass() + "]");
155 						result = value.toString();
156 					}
157 				}
158 				return result != null ? result : typeConverter.convertIfNecessary(value, requiredType, methodParam);
159 			}
160 
161 			@SuppressWarnings("rawtypes")
162 			public Object convertIfNecessary(Object value, Class requiredType) throws TypeMismatchException {
163 				return convertIfNecessary(value, requiredType, null);
164 			}
165 		};
166 		beanFactory.setTypeConverter(contextTypeConverter);
167 
168 		try {
169 
170 			/*
171 			 * Need to use the merged bean definition here, otherwise it gets
172 			 * cached and "frozen" in and the "regular" bean definition does not
173 			 * come back when getBean() is called later on
174 			 */
175 			String targetBeanName = getTargetBeanName();
176 			GenericBeanDefinition beanDefinition = new GenericBeanDefinition(listableBeanFactory
177 					.getMergedBeanDefinition(targetBeanName));
178 			logger.debug("Rehydrating scoped target: [" + targetBeanName + "]");
179 
180 			BeanDefinitionVisitor visitor = new PlaceholderBeanDefinitionVisitor(contextTypeConverter);
181 
182 			beanFactory.registerBeanDefinition(beanName, beanDefinition);
183 			// Make the replacements before the target is hydrated
184 			visitor.visitBeanDefinition(beanDefinition);
185 			target = beanFactory.getBean(beanName);
186 			putTargetInContext(target);
187 			return target;
188 
189 		}
190 		finally {
191 			beanFactory.removeBeanDefinition(beanName);
192 			beanFactory = null;
193 			// Anything else we can do to clean it up?
194 		}
195 
196 	}
197 
198 	private void putTargetInContext(Object target) {
199 		Object context = contextFactory.getContext();
200 		if (context instanceof AttributeAccessor) {
201 			((AttributeAccessor) context).setAttribute(beanName, target);
202 		}
203 	}
204 
205 	private Object getTargetFromContext() {
206 		Object context = contextFactory.getContext();
207 		if (context instanceof AttributeAccessor) {
208 			return ((AttributeAccessor) context).getAttribute(beanName);
209 		}
210 		return null;
211 	}
212 
213 	/**
214 	 * @param value
215 	 * @param typeConverter
216 	 * @return a String representation of the input if possible
217 	 */
218 	protected String convertToString(Object value, TypeConverter typeConverter) {
219 		String result = null;
220 		try {
221 			// Give it one chance to convert - this forces the default editors
222 			// to be registered
223 			result = (String) typeConverter.convertIfNecessary(value, String.class);
224 		}
225 		catch (TypeMismatchException e) {
226 			// ignore
227 		}
228 		if (result == null && typeConverter instanceof PropertyEditorRegistrySupport) {
229 			/*
230 			 * PropertyEditorRegistrySupport is de rigeur with TypeConverter
231 			 * instances used internally by Spring. If we have one of those then
232 			 * we can convert to String but the TypeConverter doesn't know how
233 			 * to.
234 			 */
235 			PropertyEditorRegistrySupport registry = (PropertyEditorRegistrySupport) typeConverter;
236 			PropertyEditor editor = registry.findCustomEditor(value.getClass(), null);
237 			if (editor != null) {
238 				if (registry.isSharedEditor(editor)) {
239 					// Synchronized access to shared editor
240 					// instance.
241 					synchronized (editor) {
242 						editor.setValue(value);
243 						result = editor.getAsText();
244 					}
245 				}
246 				else {
247 					editor.setValue(value);
248 					result = editor.getAsText();
249 				}
250 			}
251 		}
252 		return result;
253 	}
254 
255 	/**
256 	 * @param value
257 	 * @param requiredType
258 	 * @return
259 	 */
260 	private Object convertFromContext(String key, Class<?> requiredType) {
261 		Object result = null;
262 		Object property = getPropertyFromContext(key);
263 		if (property == null || requiredType.isAssignableFrom(property.getClass())) {
264 			result = property;
265 		}
266 		return result;
267 	}
268 
269 	private Object getPropertyFromContext(String key) {
270 		Object context = contextFactory.getContext();
271 		if (context == null) {
272 			throw new IllegalStateException("No context available while replacing placeholders.");
273 		}
274 		BeanWrapper wrapper = new BeanWrapperImpl(context);
275 		if (wrapper.isReadableProperty(key)) {
276 			return wrapper.getPropertyValue(key);
277 		}
278 		return null;
279 	}
280 
281 	private String extractKey(String value) {
282 		return value.substring(value.indexOf(PLACEHOLDER_PREFIX) + PLACEHOLDER_PREFIX.length(), value
283 				.indexOf(PLACEHOLDER_SUFFIX));
284 	}
285 
286 	/**
287 	 * Determine whether the input is a whole key in the form
288 	 * <code>%{...}</code>, i.e. starting with the correct prefix, ending with
289 	 * the correct suffix and containing only one of each.
290 	 * 
291 	 * @param value a String with placeholder patterns
292 	 * @return true if the value is a key
293 	 */
294 	private boolean isKey(String value) {
295 		return value.indexOf(PLACEHOLDER_PREFIX) == value.lastIndexOf(PLACEHOLDER_PREFIX)
296 				&& value.startsWith(PLACEHOLDER_PREFIX) && value.endsWith(PLACEHOLDER_SUFFIX);
297 	}
298 
299 	/**
300 	 * A {@link BeanDefinitionVisitor} that will replace embedded placeholders
301 	 * with values from the provided context.
302 	 * 
303 	 * @author Dave Syer
304 	 * 
305 	 */
306 	private final class PlaceholderBeanDefinitionVisitor extends BeanDefinitionVisitor {
307 
308 		public PlaceholderBeanDefinitionVisitor(final TypeConverter typeConverter) {
309 			super(new PlaceholderStringValueResolver(typeConverter));
310 		}
311 
312 		@SuppressWarnings({ "unchecked", "rawtypes" })
313 		protected Object resolveValue(Object value) {
314 
315 			if (value instanceof TypedStringValue) {
316 
317 				TypedStringValue typedStringValue = (TypedStringValue) value;
318 				String stringValue = typedStringValue.getValue();
319 				if (stringValue != null) {
320 
321 					// If the value is a whole key, try to simply replace it
322 					// from context.
323 					if (isKey(stringValue)) {
324 						String key = extractKey(stringValue);
325 						Object result = getPropertyFromContext(key);
326 						if (result != null) {
327 							value = result;
328 							logger.debug(String.format("Resolved %%{%s} to obtain [%s]", key, result));
329 						}
330 					}
331 					else {
332 						// Otherwise it might contain embedded keys so we try to
333 						// replace those
334 						String visitedString = resolveStringValue(stringValue);
335 						value = new TypedStringValue(visitedString);
336 					}
337 				}
338 
339 			}
340 			else if (value instanceof Map) {
341 
342 				Map map = (Map) value;
343 				Map newValue = new ManagedMap(map.size());
344 				newValue.putAll(map);
345 				super.visitMap(newValue);
346 				value = newValue;
347 
348 			}
349 			else if (value instanceof List) {
350 
351 				List list = (List) value;
352 				List newValue = new ManagedList(list.size());
353 				newValue.addAll(list);
354 				super.visitList(newValue);
355 				value = newValue;
356 
357 			}
358 			else if (value instanceof Set) {
359 
360 				Set list = (Set) value;
361 				Set newValue = new ManagedSet(list.size());
362 				newValue.addAll(list);
363 				super.visitSet(newValue);
364 				value = newValue;
365 
366 			}
367 			else if (value instanceof BeanDefinition) {
368 
369 				BeanDefinition newValue = new GenericBeanDefinition((BeanDefinition) value);
370 				visitBeanDefinition((BeanDefinition) newValue);
371 				value = newValue;
372 
373 			}
374 			else if (value instanceof BeanDefinitionHolder) {
375 
376 				BeanDefinition newValue = new GenericBeanDefinition(((BeanDefinitionHolder) value).getBeanDefinition());
377 				visitBeanDefinition((BeanDefinition) newValue);
378 				value = newValue;
379 
380 			}
381 			else {
382 
383 				value = super.resolveValue(value);
384 
385 			}
386 
387 			return value;
388 
389 		}
390 
391 	}
392 
393 	private final class PlaceholderStringValueResolver implements StringValueResolver {
394 
395 		private final TypeConverter typeConverter;
396 
397 		private PlaceholderStringValueResolver(TypeConverter typeConverter) {
398 			this.typeConverter = typeConverter;
399 		}
400 
401 		public String resolveStringValue(String strVal) {
402 			if (!strVal.contains(PLACEHOLDER_PREFIX)) {
403 				return strVal;
404 			}
405 			return replacePlaceholders(strVal, typeConverter);
406 		}
407 
408 		/**
409 		 * Convenience method to replace all the placeholders in the input.
410 		 * 
411 		 * @param typeConverter a {@link TypeConverter} that can be used to
412 		 * convert placeholder keys to context values
413 		 * @param value the value to replace placeholders in
414 		 * @return the input with placeholders replaced
415 		 */
416 		private String replacePlaceholders(String value, TypeConverter typeConverter) {
417 
418 			StringBuilder result = new StringBuilder(value);
419 
420 			int first = result.indexOf(PLACEHOLDER_PREFIX);
421 			int next = result.indexOf(PLACEHOLDER_SUFFIX, first + 1);
422 
423 			while (first >= 0) {
424 
425 				Assert.state(next > 0, String.format("Placeholder key incorrectly specified: use %skey%s (in %s)",
426 						PLACEHOLDER_PREFIX, PLACEHOLDER_SUFFIX, value));
427 
428 				String key = result.substring(first + PLACEHOLDER_PREFIX.length(), next);
429 
430 				boolean replaced = replaceIfTypeMatches(result, first, next, key, String.class, typeConverter);
431 				replaced |= replaceIfTypeMatches(result, first, next, key, Long.class, typeConverter);
432 				replaced |= replaceIfTypeMatches(result, first, next, key, Integer.class, typeConverter);
433 				replaced |= replaceIfTypeMatches(result, first, next, key, Date.class, typeConverter);
434 				if (!replaced) {
435 					if (!value.startsWith(PLACEHOLDER_PREFIX) || !value.endsWith(PLACEHOLDER_SUFFIX)) {
436 						throw new IllegalStateException(String.format("Cannot bind to partial key %%{%s} in %s", key,
437 								value));
438 					}
439 					logger.debug(String.format("Deferring binding of placeholder: %%{%s}", key));
440 				}
441 				else {
442 					logger.debug(String.format("Bound %%{%s} to obtain [%s]", key, result));
443 				}
444 				first = result.indexOf(PLACEHOLDER_PREFIX, first + 1);
445 				next = result.indexOf(PLACEHOLDER_SUFFIX, first + 1);
446 
447 			}
448 
449 			return result.toString();
450 
451 		}
452 
453 		private boolean replaceIfTypeMatches(StringBuilder result, int first, int next, String key,
454 				Class<?> requiredType, TypeConverter typeConverter) {
455 			Object property = convertFromContext(key, requiredType);
456 			if (property != null) {
457 				result.replace(first, next + 1, (String) typeConverter.convertIfNecessary(property, String.class));
458 				return true;
459 			}
460 			return false;
461 		}
462 
463 	}
464 
465 }