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}