001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018package org.apache.commons.configuration2; 019 020import java.util.ArrayList; 021import java.util.Arrays; 022import java.util.HashSet; 023import java.util.Iterator; 024import java.util.List; 025import java.util.Set; 026 027import javax.naming.Context; 028import javax.naming.InitialContext; 029import javax.naming.NameClassPair; 030import javax.naming.NameNotFoundException; 031import javax.naming.NamingEnumeration; 032import javax.naming.NamingException; 033import javax.naming.NotContextException; 034 035import org.apache.commons.configuration2.event.ConfigurationErrorEvent; 036import org.apache.commons.configuration2.io.ConfigurationLogger; 037import org.apache.commons.lang3.StringUtils; 038 039/** 040 * This Configuration class allows you to interface with a JNDI datasource. A JNDIConfiguration is read-only, write 041 * operations will throw an UnsupportedOperationException. The clear operations are supported but the underlying JNDI 042 * data source is not changed. 043 */ 044public class JNDIConfiguration extends AbstractConfiguration { 045 /** The prefix of the context. */ 046 private String prefix; 047 048 /** The initial JNDI context. */ 049 private Context context; 050 051 /** The base JNDI context. */ 052 private Context baseContext; 053 054 /** The Set of keys that have been virtually cleared. */ 055 private final Set<String> clearedProperties = new HashSet<>(); 056 057 /** 058 * Creates a JNDIConfiguration using the default initial context as the root of the properties. 059 * 060 * @throws NamingException thrown if an error occurs when initializing the default context 061 */ 062 public JNDIConfiguration() throws NamingException { 063 this((String) null); 064 } 065 066 /** 067 * Creates a JNDIConfiguration using the default initial context, shifted with the specified prefix, as the root of the 068 * properties. 069 * 070 * @param prefix the prefix 071 * 072 * @throws NamingException thrown if an error occurs when initializing the default context 073 */ 074 public JNDIConfiguration(final String prefix) throws NamingException { 075 this(new InitialContext(), prefix); 076 } 077 078 /** 079 * Creates a JNDIConfiguration using the specified initial context as the root of the properties. 080 * 081 * @param context the initial context 082 */ 083 public JNDIConfiguration(final Context context) { 084 this(context, null); 085 } 086 087 /** 088 * Creates a JNDIConfiguration using the specified initial context shifted by the specified prefix as the root of the 089 * properties. 090 * 091 * @param context the initial context 092 * @param prefix the prefix 093 */ 094 public JNDIConfiguration(final Context context, final String prefix) { 095 this.context = context; 096 this.prefix = prefix; 097 initLogger(new ConfigurationLogger(JNDIConfiguration.class)); 098 addErrorLogListener(); 099 } 100 101 /** 102 * This method recursive traverse the JNDI tree, looking for Context objects. When it finds them, it traverses them as 103 * well. Otherwise it just adds the values to the list of keys found. 104 * 105 * @param keys All the keys that have been found. 106 * @param context The parent context 107 * @param prefix What prefix we are building on. 108 * @param processedCtx a set with the so far processed objects 109 * @throws NamingException If JNDI has an issue. 110 */ 111 private void recursiveGetKeys(final Set<String> keys, final Context context, final String prefix, final Set<Context> processedCtx) throws NamingException { 112 processedCtx.add(context); 113 NamingEnumeration<NameClassPair> elements = null; 114 115 try { 116 elements = context.list(""); 117 118 // iterates through the context's elements 119 while (elements.hasMore()) { 120 final NameClassPair nameClassPair = elements.next(); 121 final String name = nameClassPair.getName(); 122 final Object object = context.lookup(name); 123 124 // build the key 125 final StringBuilder keyBuilder = new StringBuilder(); 126 keyBuilder.append(prefix); 127 if (keyBuilder.length() > 0) { 128 keyBuilder.append("."); 129 } 130 keyBuilder.append(name); 131 132 if (object instanceof Context) { 133 // add the keys of the sub context 134 final Context subcontext = (Context) object; 135 if (!processedCtx.contains(subcontext)) { 136 recursiveGetKeys(keys, subcontext, keyBuilder.toString(), processedCtx); 137 } 138 } else { 139 // add the key 140 keys.add(keyBuilder.toString()); 141 } 142 } 143 } finally { 144 // close the enumeration 145 if (elements != null) { 146 elements.close(); 147 } 148 } 149 } 150 151 /** 152 * Gets an iterator with all property keys stored in this configuration. 153 * 154 * @return an iterator with all keys 155 */ 156 @Override 157 protected Iterator<String> getKeysInternal() { 158 return getKeysInternal(""); 159 } 160 161 /** 162 * Gets an iterator with all property keys starting with the given prefix. 163 * 164 * @param prefix the prefix 165 * @return an iterator with the selected keys 166 */ 167 @Override 168 protected Iterator<String> getKeysInternal(final String prefix) { 169 // build the path 170 final String[] splitPath = StringUtils.split(prefix, "."); 171 172 final List<String> path = Arrays.asList(splitPath); 173 174 try { 175 // find the context matching the specified path 176 final Context context = getContext(path, getBaseContext()); 177 178 // return all the keys under the context found 179 final Set<String> keys = new HashSet<>(); 180 if (context != null) { 181 recursiveGetKeys(keys, context, prefix, new HashSet<>()); 182 } else if (containsKey(prefix)) { 183 // add the prefix if it matches exactly a property key 184 keys.add(prefix); 185 } 186 187 return keys.iterator(); 188 } catch (final NameNotFoundException e) { 189 // expected exception, no need to log it 190 return new ArrayList<String>().iterator(); 191 } catch (final NamingException e) { 192 fireError(ConfigurationErrorEvent.READ, ConfigurationErrorEvent.READ, null, null, e); 193 return new ArrayList<String>().iterator(); 194 } 195 } 196 197 /** 198 * Because JNDI is based on a tree configuration, we need to filter down the tree, till we find the Context specified by 199 * the key to start from. Otherwise return null. 200 * 201 * @param path the path of keys to traverse in order to find the context 202 * @param context the context to start from 203 * @return The context at that key's location in the JNDI tree, or null if not found 204 * @throws NamingException if JNDI has an issue 205 */ 206 private Context getContext(final List<String> path, final Context context) throws NamingException { 207 // return the current context if the path is empty 208 if (path == null || path.isEmpty()) { 209 return context; 210 } 211 212 final String key = path.get(0); 213 214 // search a context matching the key in the context's elements 215 NamingEnumeration<NameClassPair> elements = null; 216 217 try { 218 elements = context.list(""); 219 while (elements.hasMore()) { 220 final NameClassPair nameClassPair = elements.next(); 221 final String name = nameClassPair.getName(); 222 final Object object = context.lookup(name); 223 224 if (object instanceof Context && name.equals(key)) { 225 final Context subcontext = (Context) object; 226 227 // recursive search in the sub context 228 return getContext(path.subList(1, path.size()), subcontext); 229 } 230 } 231 } finally { 232 if (elements != null) { 233 elements.close(); 234 } 235 } 236 237 return null; 238 } 239 240 /** 241 * Returns a flag whether this configuration is empty. 242 * 243 * @return the empty flag 244 */ 245 @Override 246 protected boolean isEmptyInternal() { 247 try { 248 NamingEnumeration<NameClassPair> enumeration = null; 249 250 try { 251 enumeration = getBaseContext().list(""); 252 return !enumeration.hasMore(); 253 } finally { 254 // close the enumeration 255 if (enumeration != null) { 256 enumeration.close(); 257 } 258 } 259 } catch (final NamingException e) { 260 fireError(ConfigurationErrorEvent.READ, ConfigurationErrorEvent.READ, null, null, e); 261 return true; 262 } 263 } 264 265 /** 266 * <p> 267 * <strong>This operation is not supported and will throw an UnsupportedOperationException.</strong> 268 * </p> 269 * 270 * @param key the key 271 * @param value the value 272 * @throws UnsupportedOperationException always thrown as this method is not supported 273 */ 274 @Override 275 protected void setPropertyInternal(final String key, final Object value) { 276 throw new UnsupportedOperationException("This operation is not supported"); 277 } 278 279 /** 280 * Removes the specified property. 281 * 282 * @param key the key of the property to remove 283 */ 284 @Override 285 protected void clearPropertyDirect(final String key) { 286 clearedProperties.add(key); 287 } 288 289 /** 290 * Checks whether the specified key is contained in this configuration. 291 * 292 * @param key the key to check 293 * @return a flag whether this key is stored in this configuration 294 */ 295 @Override 296 protected boolean containsKeyInternal(String key) { 297 if (clearedProperties.contains(key)) { 298 return false; 299 } 300 key = key.replace('.', '/'); 301 try { 302 // throws a NamingException if JNDI doesn't contain the key. 303 getBaseContext().lookup(key); 304 return true; 305 } catch (final NameNotFoundException e) { 306 // expected exception, no need to log it 307 return false; 308 } catch (final NamingException e) { 309 fireError(ConfigurationErrorEvent.READ, ConfigurationErrorEvent.READ, key, null, e); 310 return false; 311 } 312 } 313 314 /** 315 * Gets the prefix. 316 * 317 * @return the prefix 318 */ 319 public String getPrefix() { 320 return prefix; 321 } 322 323 /** 324 * Sets the prefix. 325 * 326 * @param prefix The prefix to set 327 */ 328 public void setPrefix(final String prefix) { 329 this.prefix = prefix; 330 331 // clear the previous baseContext 332 baseContext = null; 333 } 334 335 /** 336 * Gets the value of the specified property. 337 * 338 * @param key the key of the property 339 * @return the value of this property 340 */ 341 @Override 342 protected Object getPropertyInternal(String key) { 343 if (clearedProperties.contains(key)) { 344 return null; 345 } 346 347 try { 348 key = key.replace('.', '/'); 349 return getBaseContext().lookup(key); 350 } catch (final NameNotFoundException | NotContextException nctxex) { 351 // expected exception, no need to log it 352 return null; 353 } catch (final NamingException e) { 354 fireError(ConfigurationErrorEvent.READ, ConfigurationErrorEvent.READ, key, null, e); 355 return null; 356 } 357 } 358 359 /** 360 * <p> 361 * <strong>This operation is not supported and will throw an UnsupportedOperationException.</strong> 362 * </p> 363 * 364 * @param key the key 365 * @param obj the value 366 * @throws UnsupportedOperationException always thrown as this method is not supported 367 */ 368 @Override 369 protected void addPropertyDirect(final String key, final Object obj) { 370 throw new UnsupportedOperationException("This operation is not supported"); 371 } 372 373 /** 374 * Gets the base context with the prefix applied. 375 * 376 * @return the base context 377 * @throws NamingException if an error occurs 378 */ 379 public Context getBaseContext() throws NamingException { 380 if (baseContext == null) { 381 baseContext = (Context) getContext().lookup(prefix == null ? "" : prefix); 382 } 383 384 return baseContext; 385 } 386 387 /** 388 * Gets the initial context used by this configuration. This context is independent of the prefix specified. 389 * 390 * @return the initial context 391 */ 392 public Context getContext() { 393 return context; 394 } 395 396 /** 397 * Sets the initial context of the configuration. 398 * 399 * @param context the context 400 */ 401 public void setContext(final Context context) { 402 // forget the removed properties 403 clearedProperties.clear(); 404 405 // change the context 406 this.context = context; 407 } 408}