001/*
002 * Copyright 2002-2015 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.jndi;
018
019import java.lang.reflect.Method;
020import java.lang.reflect.Modifier;
021import javax.naming.Context;
022import javax.naming.NamingException;
023
024import org.aopalliance.intercept.MethodInterceptor;
025import org.aopalliance.intercept.MethodInvocation;
026
027import org.springframework.aop.framework.ProxyFactory;
028import org.springframework.beans.SimpleTypeConverter;
029import org.springframework.beans.TypeConverter;
030import org.springframework.beans.TypeMismatchException;
031import org.springframework.beans.factory.BeanClassLoaderAware;
032import org.springframework.beans.factory.BeanFactory;
033import org.springframework.beans.factory.BeanFactoryAware;
034import org.springframework.beans.factory.FactoryBean;
035import org.springframework.beans.factory.config.ConfigurableBeanFactory;
036import org.springframework.util.ClassUtils;
037
038/**
039 * {@link org.springframework.beans.factory.FactoryBean} that looks up a
040 * JNDI object. Exposes the object found in JNDI for bean references,
041 * e.g. for data access object's "dataSource" property in case of a
042 * {@link javax.sql.DataSource}.
043 *
044 * <p>The typical usage will be to register this as singleton factory
045 * (e.g. for a certain JNDI-bound DataSource) in an application context,
046 * and give bean references to application services that need it.
047 *
048 * <p>The default behavior is to look up the JNDI object on startup and cache it.
049 * This can be customized through the "lookupOnStartup" and "cache" properties,
050 * using a {@link JndiObjectTargetSource} underneath. Note that you need to specify
051 * a "proxyInterface" in such a scenario, since the actual JNDI object type is not
052 * known in advance.
053 *
054 * <p>Of course, bean classes in a Spring environment may lookup e.g. a DataSource
055 * from JNDI themselves. This class simply enables central configuration of the
056 * JNDI name, and easy switching to non-JNDI alternatives. The latter is
057 * particularly convenient for test setups, reuse in standalone clients, etc.
058 *
059 * <p>Note that switching to e.g. DriverManagerDataSource is just a matter of
060 * configuration: Simply replace the definition of this FactoryBean with a
061 * {@link org.springframework.jdbc.datasource.DriverManagerDataSource} definition!
062 *
063 * @author Juergen Hoeller
064 * @since 22.05.2003
065 * @see #setProxyInterface
066 * @see #setLookupOnStartup
067 * @see #setCache
068 * @see JndiObjectTargetSource
069 */
070public class JndiObjectFactoryBean extends JndiObjectLocator
071                implements FactoryBean<Object>, BeanFactoryAware, BeanClassLoaderAware {
072
073        private Class<?>[] proxyInterfaces;
074
075        private boolean lookupOnStartup = true;
076
077        private boolean cache = true;
078
079        private boolean exposeAccessContext = false;
080
081        private Object defaultObject;
082
083        private ConfigurableBeanFactory beanFactory;
084
085        private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader();
086
087        private Object jndiObject;
088
089
090        /**
091         * Specify the proxy interface to use for the JNDI object.
092         * <p>Typically used in conjunction with "lookupOnStartup"=false and/or "cache"=false.
093         * Needs to be specified because the actual JNDI object type is not known
094         * in advance in case of a lazy lookup.
095         * @see #setProxyInterfaces
096         * @see #setLookupOnStartup
097         * @see #setCache
098         */
099        public void setProxyInterface(Class<?> proxyInterface) {
100                this.proxyInterfaces = new Class<?>[] {proxyInterface};
101        }
102
103        /**
104         * Specify multiple proxy interfaces to use for the JNDI object.
105         * <p>Typically used in conjunction with "lookupOnStartup"=false and/or "cache"=false.
106         * Note that proxy interfaces will be autodetected from a specified "expectedType",
107         * if necessary.
108         * @see #setExpectedType
109         * @see #setLookupOnStartup
110         * @see #setCache
111         */
112        public void setProxyInterfaces(Class<?>... proxyInterfaces) {
113                this.proxyInterfaces = proxyInterfaces;
114        }
115
116        /**
117         * Set whether to look up the JNDI object on startup. Default is "true".
118         * <p>Can be turned off to allow for late availability of the JNDI object.
119         * In this case, the JNDI object will be fetched on first access.
120         * <p>For a lazy lookup, a proxy interface needs to be specified.
121         * @see #setProxyInterface
122         * @see #setCache
123         */
124        public void setLookupOnStartup(boolean lookupOnStartup) {
125                this.lookupOnStartup = lookupOnStartup;
126        }
127
128        /**
129         * Set whether to cache the JNDI object once it has been located.
130         * Default is "true".
131         * <p>Can be turned off to allow for hot redeployment of JNDI objects.
132         * In this case, the JNDI object will be fetched for each invocation.
133         * <p>For hot redeployment, a proxy interface needs to be specified.
134         * @see #setProxyInterface
135         * @see #setLookupOnStartup
136         */
137        public void setCache(boolean cache) {
138                this.cache = cache;
139        }
140
141        /**
142         * Set whether to expose the JNDI environment context for all access to the target
143         * object, i.e. for all method invocations on the exposed object reference.
144         * <p>Default is "false", i.e. to only expose the JNDI context for object lookup.
145         * Switch this flag to "true" in order to expose the JNDI environment (including
146         * the authorization context) for each method invocation, as needed by WebLogic
147         * for JNDI-obtained factories (e.g. JDBC DataSource, JMS ConnectionFactory)
148         * with authorization requirements.
149         */
150        public void setExposeAccessContext(boolean exposeAccessContext) {
151                this.exposeAccessContext = exposeAccessContext;
152        }
153
154        /**
155         * Specify a default object to fall back to if the JNDI lookup fails.
156         * Default is none.
157         * <p>This can be an arbitrary bean reference or literal value.
158         * It is typically used for literal values in scenarios where the JNDI environment
159         * might define specific config settings but those are not required to be present.
160         * <p>Note: This is only supported for lookup on startup.
161         * If specified together with {@link #setExpectedType}, the specified value
162         * needs to be either of that type or convertible to it.
163         * @see #setLookupOnStartup
164         * @see ConfigurableBeanFactory#getTypeConverter()
165         * @see SimpleTypeConverter
166         */
167        public void setDefaultObject(Object defaultObject) {
168                this.defaultObject = defaultObject;
169        }
170
171        @Override
172        public void setBeanFactory(BeanFactory beanFactory) {
173                if (beanFactory instanceof ConfigurableBeanFactory) {
174                        // Just optional - for getting a specifically configured TypeConverter if needed.
175                        // We'll simply fall back to a SimpleTypeConverter if no specific one available.
176                        this.beanFactory = (ConfigurableBeanFactory) beanFactory;
177                }
178        }
179
180        @Override
181        public void setBeanClassLoader(ClassLoader classLoader) {
182                this.beanClassLoader = classLoader;
183        }
184
185
186        /**
187         * Look up the JNDI object and store it.
188         */
189        @Override
190        public void afterPropertiesSet() throws IllegalArgumentException, NamingException {
191                super.afterPropertiesSet();
192
193                if (this.proxyInterfaces != null || !this.lookupOnStartup || !this.cache || this.exposeAccessContext) {
194                        // We need to create a proxy for this...
195                        if (this.defaultObject != null) {
196                                throw new IllegalArgumentException(
197                                                "'defaultObject' is not supported in combination with 'proxyInterface'");
198                        }
199                        // We need a proxy and a JndiObjectTargetSource.
200                        this.jndiObject = JndiObjectProxyFactory.createJndiObjectProxy(this);
201                }
202                else {
203                        if (this.defaultObject != null && getExpectedType() != null &&
204                                        !getExpectedType().isInstance(this.defaultObject)) {
205                                TypeConverter converter = (this.beanFactory != null ?
206                                                this.beanFactory.getTypeConverter() : new SimpleTypeConverter());
207                                try {
208                                        this.defaultObject = converter.convertIfNecessary(this.defaultObject, getExpectedType());
209                                }
210                                catch (TypeMismatchException ex) {
211                                        throw new IllegalArgumentException("Default object [" + this.defaultObject + "] of type [" +
212                                                        this.defaultObject.getClass().getName() + "] is not of expected type [" +
213                                                        getExpectedType().getName() + "] and cannot be converted either", ex);
214                                }
215                        }
216                        // Locate specified JNDI object.
217                        this.jndiObject = lookupWithFallback();
218                }
219        }
220
221        /**
222         * Lookup variant that returns the specified "defaultObject"
223         * (if any) in case of lookup failure.
224         * @return the located object, or the "defaultObject" as fallback
225         * @throws NamingException in case of lookup failure without fallback
226         * @see #setDefaultObject
227         */
228        protected Object lookupWithFallback() throws NamingException {
229                ClassLoader originalClassLoader = ClassUtils.overrideThreadContextClassLoader(this.beanClassLoader);
230                try {
231                        return lookup();
232                }
233                catch (TypeMismatchNamingException ex) {
234                        // Always let TypeMismatchNamingException through -
235                        // we don't want to fall back to the defaultObject in this case.
236                        throw ex;
237                }
238                catch (NamingException ex) {
239                        if (this.defaultObject != null) {
240                                if (logger.isDebugEnabled()) {
241                                        logger.debug("JNDI lookup failed - returning specified default object instead", ex);
242                                }
243                                else if (logger.isInfoEnabled()) {
244                                        logger.info("JNDI lookup failed - returning specified default object instead: " + ex);
245                                }
246                                return this.defaultObject;
247                        }
248                        throw ex;
249                }
250                finally {
251                        if (originalClassLoader != null) {
252                                Thread.currentThread().setContextClassLoader(originalClassLoader);
253                        }
254                }
255        }
256
257
258        /**
259         * Return the singleton JNDI object.
260         */
261        @Override
262        public Object getObject() {
263                return this.jndiObject;
264        }
265
266        @Override
267        public Class<?> getObjectType() {
268                if (this.proxyInterfaces != null) {
269                        if (this.proxyInterfaces.length == 1) {
270                                return this.proxyInterfaces[0];
271                        }
272                        else if (this.proxyInterfaces.length > 1) {
273                                return createCompositeInterface(this.proxyInterfaces);
274                        }
275                }
276                if (this.jndiObject != null) {
277                        return this.jndiObject.getClass();
278                }
279                else {
280                        return getExpectedType();
281                }
282        }
283
284        @Override
285        public boolean isSingleton() {
286                return true;
287        }
288
289
290        /**
291         * Create a composite interface Class for the given interfaces,
292         * implementing the given interfaces in one single Class.
293         * <p>The default implementation builds a JDK proxy class for the
294         * given interfaces.
295         * @param interfaces the interfaces to merge
296         * @return the merged interface as Class
297         * @see java.lang.reflect.Proxy#getProxyClass
298         */
299        protected Class<?> createCompositeInterface(Class<?>[] interfaces) {
300                return ClassUtils.createCompositeInterface(interfaces, this.beanClassLoader);
301        }
302
303
304        /**
305         * Inner class to just introduce an AOP dependency when actually creating a proxy.
306         */
307        private static class JndiObjectProxyFactory {
308
309                private static Object createJndiObjectProxy(JndiObjectFactoryBean jof) throws NamingException {
310                        // Create a JndiObjectTargetSource that mirrors the JndiObjectFactoryBean's configuration.
311                        JndiObjectTargetSource targetSource = new JndiObjectTargetSource();
312                        targetSource.setJndiTemplate(jof.getJndiTemplate());
313                        targetSource.setJndiName(jof.getJndiName());
314                        targetSource.setExpectedType(jof.getExpectedType());
315                        targetSource.setResourceRef(jof.isResourceRef());
316                        targetSource.setLookupOnStartup(jof.lookupOnStartup);
317                        targetSource.setCache(jof.cache);
318                        targetSource.afterPropertiesSet();
319
320                        // Create a proxy with JndiObjectFactoryBean's proxy interface and the JndiObjectTargetSource.
321                        ProxyFactory proxyFactory = new ProxyFactory();
322                        if (jof.proxyInterfaces != null) {
323                                proxyFactory.setInterfaces(jof.proxyInterfaces);
324                        }
325                        else {
326                                Class<?> targetClass = targetSource.getTargetClass();
327                                if (targetClass == null) {
328                                        throw new IllegalStateException(
329                                                        "Cannot deactivate 'lookupOnStartup' without specifying a 'proxyInterface' or 'expectedType'");
330                                }
331                                Class<?>[] ifcs = ClassUtils.getAllInterfacesForClass(targetClass, jof.beanClassLoader);
332                                for (Class<?> ifc : ifcs) {
333                                        if (Modifier.isPublic(ifc.getModifiers())) {
334                                                proxyFactory.addInterface(ifc);
335                                        }
336                                }
337                        }
338                        if (jof.exposeAccessContext) {
339                                proxyFactory.addAdvice(new JndiContextExposingInterceptor(jof.getJndiTemplate()));
340                        }
341                        proxyFactory.setTargetSource(targetSource);
342                        return proxyFactory.getProxy(jof.beanClassLoader);
343                }
344        }
345
346
347        /**
348         * Interceptor that exposes the JNDI context for all method invocations,
349         * according to JndiObjectFactoryBean's "exposeAccessContext" flag.
350         */
351        private static class JndiContextExposingInterceptor implements MethodInterceptor {
352
353                private final JndiTemplate jndiTemplate;
354
355                public JndiContextExposingInterceptor(JndiTemplate jndiTemplate) {
356                        this.jndiTemplate = jndiTemplate;
357                }
358
359                @Override
360                public Object invoke(MethodInvocation invocation) throws Throwable {
361                        Context ctx = (isEligible(invocation.getMethod()) ? this.jndiTemplate.getContext() : null);
362                        try {
363                                return invocation.proceed();
364                        }
365                        finally {
366                                this.jndiTemplate.releaseContext(ctx);
367                        }
368                }
369
370                protected boolean isEligible(Method method) {
371                        return (Object.class != method.getDeclaringClass());
372                }
373        }
374
375}