001/*
002 * Copyright 2002-2017 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.beans.factory.config;
018
019import org.apache.commons.logging.Log;
020import org.apache.commons.logging.LogFactory;
021
022import org.springframework.beans.BeanWrapper;
023import org.springframework.beans.BeansException;
024import org.springframework.beans.PropertyAccessorFactory;
025import org.springframework.beans.factory.BeanFactory;
026import org.springframework.beans.factory.BeanFactoryAware;
027import org.springframework.beans.factory.BeanFactoryUtils;
028import org.springframework.beans.factory.BeanNameAware;
029import org.springframework.beans.factory.FactoryBean;
030import org.springframework.lang.Nullable;
031import org.springframework.util.Assert;
032import org.springframework.util.StringUtils;
033
034/**
035 * {@link FactoryBean} that evaluates a property path on a given target object.
036 *
037 * <p>The target object can be specified directly or via a bean name.
038 *
039 * <p>Usage examples:
040 *
041 * <pre class="code">&lt;!-- target bean to be referenced by name --&gt;
042 * &lt;bean id="tb" class="org.springframework.beans.TestBean" singleton="false"&gt;
043 *   &lt;property name="age" value="10"/&gt;
044 *   &lt;property name="spouse"&gt;
045 *     &lt;bean class="org.springframework.beans.TestBean"&gt;
046 *       &lt;property name="age" value="11"/&gt;
047 *     &lt;/bean&gt;
048 *   &lt;/property&gt;
049 * &lt;/bean&gt;
050 *
051 * &lt;!-- will result in 12, which is the value of property 'age' of the inner bean --&gt;
052 * &lt;bean id="propertyPath1" class="org.springframework.beans.factory.config.PropertyPathFactoryBean"&gt;
053 *   &lt;property name="targetObject"&gt;
054 *     &lt;bean class="org.springframework.beans.TestBean"&gt;
055 *       &lt;property name="age" value="12"/&gt;
056 *     &lt;/bean&gt;
057 *   &lt;/property&gt;
058 *   &lt;property name="propertyPath" value="age"/&gt;
059 * &lt;/bean&gt;
060 *
061 * &lt;!-- will result in 11, which is the value of property 'spouse.age' of bean 'tb' --&gt;
062 * &lt;bean id="propertyPath2" class="org.springframework.beans.factory.config.PropertyPathFactoryBean"&gt;
063 *   &lt;property name="targetBeanName" value="tb"/&gt;
064 *   &lt;property name="propertyPath" value="spouse.age"/&gt;
065 * &lt;/bean&gt;
066 *
067 * &lt;!-- will result in 10, which is the value of property 'age' of bean 'tb' --&gt;
068 * &lt;bean id="tb.age" class="org.springframework.beans.factory.config.PropertyPathFactoryBean"/&gt;</pre>
069 *
070 * <p>If you are using Spring 2.0 and XML Schema support in your configuration file(s),
071 * you can also use the following style of configuration for property path access.
072 * (See also the appendix entitled 'XML Schema-based configuration' in the Spring
073 * reference manual for more examples.)
074 *
075 * <pre class="code"> &lt;!-- will result in 10, which is the value of property 'age' of bean 'tb' --&gt;
076 * &lt;util:property-path id="name" path="testBean.age"/&gt;</pre>
077 *
078 * Thanks to Matthias Ernst for the suggestion and initial prototype!
079 *
080 * @author Juergen Hoeller
081 * @since 1.1.2
082 * @see #setTargetObject
083 * @see #setTargetBeanName
084 * @see #setPropertyPath
085 */
086public class PropertyPathFactoryBean implements FactoryBean<Object>, BeanNameAware, BeanFactoryAware {
087
088        private static final Log logger = LogFactory.getLog(PropertyPathFactoryBean.class);
089
090        @Nullable
091        private BeanWrapper targetBeanWrapper;
092
093        @Nullable
094        private String targetBeanName;
095
096        @Nullable
097        private String propertyPath;
098
099        @Nullable
100        private Class<?> resultType;
101
102        @Nullable
103        private String beanName;
104
105        @Nullable
106        private BeanFactory beanFactory;
107
108
109        /**
110         * Specify a target object to apply the property path to.
111         * Alternatively, specify a target bean name.
112         * @param targetObject a target object, for example a bean reference
113         * or an inner bean
114         * @see #setTargetBeanName
115         */
116        public void setTargetObject(Object targetObject) {
117                this.targetBeanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(targetObject);
118        }
119
120        /**
121         * Specify the name of a target bean to apply the property path to.
122         * Alternatively, specify a target object directly.
123         * @param targetBeanName the bean name to be looked up in the
124         * containing bean factory (e.g. "testBean")
125         * @see #setTargetObject
126         */
127        public void setTargetBeanName(String targetBeanName) {
128                this.targetBeanName = StringUtils.trimAllWhitespace(targetBeanName);
129        }
130
131        /**
132         * Specify the property path to apply to the target.
133         * @param propertyPath the property path, potentially nested
134         * (e.g. "age" or "spouse.age")
135         */
136        public void setPropertyPath(String propertyPath) {
137                this.propertyPath = StringUtils.trimAllWhitespace(propertyPath);
138        }
139
140        /**
141         * Specify the type of the result from evaluating the property path.
142         * <p>Note: This is not necessary for directly specified target objects
143         * or singleton target beans, where the type can be determined through
144         * introspection. Just specify this in case of a prototype target,
145         * provided that you need matching by type (for example, for autowiring).
146         * @param resultType the result type, for example "java.lang.Integer"
147         */
148        public void setResultType(Class<?> resultType) {
149                this.resultType = resultType;
150        }
151
152        /**
153         * The bean name of this PropertyPathFactoryBean will be interpreted
154         * as "beanName.property" pattern, if neither "targetObject" nor
155         * "targetBeanName" nor "propertyPath" have been specified.
156         * This allows for concise bean definitions with just an id/name.
157         */
158        @Override
159        public void setBeanName(String beanName) {
160                this.beanName = StringUtils.trimAllWhitespace(BeanFactoryUtils.originalBeanName(beanName));
161        }
162
163
164        @Override
165        public void setBeanFactory(BeanFactory beanFactory) {
166                this.beanFactory = beanFactory;
167
168                if (this.targetBeanWrapper != null && this.targetBeanName != null) {
169                        throw new IllegalArgumentException("Specify either 'targetObject' or 'targetBeanName', not both");
170                }
171
172                if (this.targetBeanWrapper == null && this.targetBeanName == null) {
173                        if (this.propertyPath != null) {
174                                throw new IllegalArgumentException(
175                                                "Specify 'targetObject' or 'targetBeanName' in combination with 'propertyPath'");
176                        }
177
178                        // No other properties specified: check bean name.
179                        int dotIndex = (this.beanName != null ? this.beanName.indexOf('.') : -1);
180                        if (dotIndex == -1) {
181                                throw new IllegalArgumentException(
182                                                "Neither 'targetObject' nor 'targetBeanName' specified, and PropertyPathFactoryBean " +
183                                                "bean name '" + this.beanName + "' does not follow 'beanName.property' syntax");
184                        }
185                        this.targetBeanName = this.beanName.substring(0, dotIndex);
186                        this.propertyPath = this.beanName.substring(dotIndex + 1);
187                }
188
189                else if (this.propertyPath == null) {
190                        // either targetObject or targetBeanName specified
191                        throw new IllegalArgumentException("'propertyPath' is required");
192                }
193
194                if (this.targetBeanWrapper == null && this.beanFactory.isSingleton(this.targetBeanName)) {
195                        // Eagerly fetch singleton target bean, and determine result type.
196                        Object bean = this.beanFactory.getBean(this.targetBeanName);
197                        this.targetBeanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(bean);
198                        this.resultType = this.targetBeanWrapper.getPropertyType(this.propertyPath);
199                }
200        }
201
202
203        @Override
204        @Nullable
205        public Object getObject() throws BeansException {
206                BeanWrapper target = this.targetBeanWrapper;
207                if (target != null) {
208                        if (logger.isWarnEnabled() && this.targetBeanName != null &&
209                                        this.beanFactory instanceof ConfigurableBeanFactory &&
210                                        ((ConfigurableBeanFactory) this.beanFactory).isCurrentlyInCreation(this.targetBeanName)) {
211                                logger.warn("Target bean '" + this.targetBeanName + "' is still in creation due to a circular " +
212                                                "reference - obtained value for property '" + this.propertyPath + "' may be outdated!");
213                        }
214                }
215                else {
216                        // Fetch prototype target bean...
217                        Assert.state(this.beanFactory != null, "No BeanFactory available");
218                        Assert.state(this.targetBeanName != null, "No target bean name specified");
219                        Object bean = this.beanFactory.getBean(this.targetBeanName);
220                        target = PropertyAccessorFactory.forBeanPropertyAccess(bean);
221                }
222                Assert.state(this.propertyPath != null, "No property path specified");
223                return target.getPropertyValue(this.propertyPath);
224        }
225
226        @Override
227        public Class<?> getObjectType() {
228                return this.resultType;
229        }
230
231        /**
232         * While this FactoryBean will often be used for singleton targets,
233         * the invoked getters for the property path might return a new object
234         * for each call, so we have to assume that we're not returning the
235         * same object for each {@link #getObject()} call.
236         */
237        @Override
238        public boolean isSingleton() {
239                return false;
240        }
241
242}