001/*
002 * Copyright 2002-2018 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.mock.jndi;
018
019import java.util.Hashtable;
020import javax.naming.Context;
021import javax.naming.NamingException;
022import javax.naming.spi.InitialContextFactory;
023import javax.naming.spi.InitialContextFactoryBuilder;
024import javax.naming.spi.NamingManager;
025
026import org.apache.commons.logging.Log;
027import org.apache.commons.logging.LogFactory;
028
029import org.springframework.util.Assert;
030import org.springframework.util.ClassUtils;
031
032/**
033 * Simple implementation of a JNDI naming context builder.
034 *
035 * <p>Mainly targeted at test environments, where each test case can
036 * configure JNDI appropriately, so that {@code new InitialContext()}
037 * will expose the required objects. Also usable for standalone applications,
038 * e.g. for binding a JDBC DataSource to a well-known JNDI location, to be
039 * able to use traditional Java EE data access code outside of a Java EE
040 * container.
041 *
042 * <p>There are various choices for DataSource implementations:
043 * <ul>
044 * <li>{@code SingleConnectionDataSource} (using the same Connection for all getConnection calls)
045 * <li>{@code DriverManagerDataSource} (creating a new Connection on each getConnection call)
046 * <li>Apache's Commons DBCP offers {@code org.apache.commons.dbcp.BasicDataSource} (a real pool)
047 * </ul>
048 *
049 * <p>Typical usage in bootstrap code:
050 *
051 * <pre class="code">
052 * SimpleNamingContextBuilder builder = new SimpleNamingContextBuilder();
053 * DataSource ds = new DriverManagerDataSource(...);
054 * builder.bind("java:comp/env/jdbc/myds", ds);
055 * builder.activate();</pre>
056 *
057 * Note that it's impossible to activate multiple builders within the same JVM,
058 * due to JNDI restrictions. Thus to configure a fresh builder repeatedly, use
059 * the following code to get a reference to either an already activated builder
060 * or a newly activated one:
061 *
062 * <pre class="code">
063 * SimpleNamingContextBuilder builder = SimpleNamingContextBuilder.emptyActivatedContextBuilder();
064 * DataSource ds = new DriverManagerDataSource(...);
065 * builder.bind("java:comp/env/jdbc/myds", ds);</pre>
066 *
067 * Note that you <i>should not</i> call {@code activate()} on a builder from
068 * this factory method, as there will already be an activated one in any case.
069 *
070 * <p>An instance of this class is only necessary at setup time.
071 * An application does not need to keep a reference to it after activation.
072 *
073 * @author Juergen Hoeller
074 * @author Rod Johnson
075 * @see #emptyActivatedContextBuilder()
076 * @see #bind(String, Object)
077 * @see #activate()
078 * @see SimpleNamingContext
079 * @see org.springframework.jdbc.datasource.SingleConnectionDataSource
080 * @see org.springframework.jdbc.datasource.DriverManagerDataSource
081 */
082public class SimpleNamingContextBuilder implements InitialContextFactoryBuilder {
083
084        /** An instance of this class bound to JNDI */
085        private static volatile SimpleNamingContextBuilder activated;
086
087        private static boolean initialized = false;
088
089        private static final Object initializationLock = new Object();
090
091
092        /**
093         * Checks if a SimpleNamingContextBuilder is active.
094         * @return the current SimpleNamingContextBuilder instance,
095         * or {@code null} if none
096         */
097        public static SimpleNamingContextBuilder getCurrentContextBuilder() {
098                return activated;
099        }
100
101        /**
102         * If no SimpleNamingContextBuilder is already configuring JNDI,
103         * create and activate one. Otherwise take the existing activated
104         * SimpleNamingContextBuilder, clear it and return it.
105         * <p>This is mainly intended for test suites that want to
106         * reinitialize JNDI bindings from scratch repeatedly.
107         * @return an empty SimpleNamingContextBuilder that can be used
108         * to control JNDI bindings
109         */
110        public static SimpleNamingContextBuilder emptyActivatedContextBuilder() throws NamingException {
111                if (activated != null) {
112                        // Clear already activated context builder.
113                        activated.clear();
114                }
115                else {
116                        // Create and activate new context builder.
117                        SimpleNamingContextBuilder builder = new SimpleNamingContextBuilder();
118                        // The activate() call will cause an assignment to the activated variable.
119                        builder.activate();
120                }
121                return activated;
122        }
123
124
125        private final Log logger = LogFactory.getLog(getClass());
126
127        private final Hashtable<String,Object> boundObjects = new Hashtable<String,Object>();
128
129
130        /**
131         * Register the context builder by registering it with the JNDI NamingManager.
132         * Note that once this has been done, {@code new InitialContext()} will always
133         * return a context from this factory. Use the {@code emptyActivatedContextBuilder()}
134         * static method to get an empty context (for example, in test methods).
135         * @throws IllegalStateException if there's already a naming context builder
136         * registered with the JNDI NamingManager
137         */
138        public void activate() throws IllegalStateException, NamingException {
139                logger.info("Activating simple JNDI environment");
140                synchronized (initializationLock) {
141                        if (!initialized) {
142                                Assert.state(!NamingManager.hasInitialContextFactoryBuilder(),
143                                                        "Cannot activate SimpleNamingContextBuilder: there is already a JNDI provider registered. " +
144                                                        "Note that JNDI is a JVM-wide service, shared at the JVM system class loader level, " +
145                                                        "with no reset option. As a consequence, a JNDI provider must only be registered once per JVM.");
146                                NamingManager.setInitialContextFactoryBuilder(this);
147                                initialized = true;
148                        }
149                }
150                activated = this;
151        }
152
153        /**
154         * Temporarily deactivate this context builder. It will remain registered with
155         * the JNDI NamingManager but will delegate to the standard JNDI InitialContextFactory
156         * (if configured) instead of exposing its own bound objects.
157         * <p>Call {@code activate()} again in order to expose this context builder's own
158         * bound objects again. Such activate/deactivate sequences can be applied any number
159         * of times (e.g. within a larger integration test suite running in the same VM).
160         * @see #activate()
161         */
162        public void deactivate() {
163                logger.info("Deactivating simple JNDI environment");
164                activated = null;
165        }
166
167        /**
168         * Clear all bindings in this context builder, while keeping it active.
169         */
170        public void clear() {
171                this.boundObjects.clear();
172        }
173
174        /**
175         * Bind the given object under the given name, for all naming contexts
176         * that this context builder will generate.
177         * @param name the JNDI name of the object (e.g. "java:comp/env/jdbc/myds")
178         * @param obj the object to bind (e.g. a DataSource implementation)
179         */
180        public void bind(String name, Object obj) {
181                if (logger.isInfoEnabled()) {
182                        logger.info("Static JNDI binding: [" + name + "] = [" + obj + "]");
183                }
184                this.boundObjects.put(name, obj);
185        }
186
187
188        /**
189         * Simple InitialContextFactoryBuilder implementation,
190         * creating a new SimpleNamingContext instance.
191         * @see SimpleNamingContext
192         */
193        @Override
194        public InitialContextFactory createInitialContextFactory(Hashtable<?,?> environment) {
195                if (activated == null && environment != null) {
196                        Object icf = environment.get(Context.INITIAL_CONTEXT_FACTORY);
197                        if (icf != null) {
198                                Class<?> icfClass;
199                                if (icf instanceof Class) {
200                                        icfClass = (Class<?>) icf;
201                                }
202                                else if (icf instanceof String) {
203                                        icfClass = ClassUtils.resolveClassName((String) icf, getClass().getClassLoader());
204                                }
205                                else {
206                                        throw new IllegalArgumentException("Invalid value type for environment key [" +
207                                                        Context.INITIAL_CONTEXT_FACTORY + "]: " + icf.getClass().getName());
208                                }
209                                if (!InitialContextFactory.class.isAssignableFrom(icfClass)) {
210                                        throw new IllegalArgumentException(
211                                                        "Specified class does not implement [" + InitialContextFactory.class.getName() + "]: " + icf);
212                                }
213                                try {
214                                        return (InitialContextFactory) icfClass.newInstance();
215                                }
216                                catch (Throwable ex) {
217                                        throw new IllegalStateException("Cannot instantiate specified InitialContextFactory: " + icf, ex);
218                                }
219                        }
220                }
221
222                // Default case...
223                return new InitialContextFactory() {
224                        @Override
225                        @SuppressWarnings("unchecked")
226                        public Context getInitialContext(Hashtable<?,?> environment) {
227                                return new SimpleNamingContext("", boundObjects, (Hashtable<String, Object>) environment);
228                        }
229                };
230        }
231
232}