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