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 javax.naming.Context; 021import javax.naming.InitialContext; 022import javax.naming.NameClassPair; 023import javax.naming.NameNotFoundException; 024import javax.naming.NamingEnumeration; 025import javax.naming.NamingException; 026import javax.naming.NotContextException; 027import java.util.ArrayList; 028import java.util.Arrays; 029import java.util.HashSet; 030import java.util.Iterator; 031import java.util.List; 032import java.util.Set; 033 034import org.apache.commons.configuration2.event.ConfigurationErrorEvent; 035import org.apache.commons.configuration2.io.ConfigurationLogger; 036import org.apache.commons.lang3.StringUtils; 037 038/** 039 * This Configuration class allows you to interface with a JNDI datasource. 040 * A JNDIConfiguration is read-only, write operations will throw an 041 * UnsupportedOperationException. The clear operations are supported but the 042 * underlying JNDI data source is not changed. 043 * 044 * @author <a href="mailto:epugh@upstate.com">Eric Pugh</a> 045 * @version $Id: JNDIConfiguration.java 1842194 2018-09-27 22:24:23Z ggregory $ 046 */ 047public class JNDIConfiguration extends AbstractConfiguration 048{ 049 /** The prefix of the context. */ 050 private String prefix; 051 052 /** The initial JNDI context. */ 053 private Context context; 054 055 /** The base JNDI context. */ 056 private Context baseContext; 057 058 /** The Set of keys that have been virtually cleared. */ 059 private final Set<String> clearedProperties = new HashSet<>(); 060 061 /** 062 * Creates a JNDIConfiguration using the default initial context as the 063 * root of the properties. 064 * 065 * @throws NamingException thrown if an error occurs when initializing the default context 066 */ 067 public JNDIConfiguration() throws NamingException 068 { 069 this((String) null); 070 } 071 072 /** 073 * Creates a JNDIConfiguration using the default initial context, shifted 074 * with the specified prefix, as the root of the properties. 075 * 076 * @param prefix the prefix 077 * 078 * @throws NamingException thrown if an error occurs when initializing the default context 079 */ 080 public JNDIConfiguration(final String prefix) throws NamingException 081 { 082 this(new InitialContext(), prefix); 083 } 084 085 /** 086 * Creates a JNDIConfiguration using the specified initial context as the 087 * root of the properties. 088 * 089 * @param context the initial context 090 */ 091 public JNDIConfiguration(final Context context) 092 { 093 this(context, null); 094 } 095 096 /** 097 * Creates a JNDIConfiguration using the specified initial context shifted 098 * by the specified prefix as the root of the properties. 099 * 100 * @param context the initial context 101 * @param prefix the prefix 102 */ 103 public JNDIConfiguration(final Context context, final String prefix) 104 { 105 this.context = context; 106 this.prefix = prefix; 107 initLogger(new ConfigurationLogger(JNDIConfiguration.class)); 108 addErrorLogListener(); 109 } 110 111 /** 112 * This method recursive traverse the JNDI tree, looking for Context objects. 113 * When it finds them, it traverses them as well. Otherwise it just adds the 114 * values to the list of keys found. 115 * 116 * @param keys All the keys that have been found. 117 * @param context The parent context 118 * @param prefix What prefix we are building on. 119 * @param processedCtx a set with the so far processed objects 120 * @throws NamingException If JNDI has an issue. 121 */ 122 private void recursiveGetKeys(final Set<String> keys, final Context context, final String prefix, 123 final Set<Context> processedCtx) throws NamingException 124 { 125 processedCtx.add(context); 126 NamingEnumeration<NameClassPair> elements = null; 127 128 try 129 { 130 elements = context.list(""); 131 132 // iterates through the context's elements 133 while (elements.hasMore()) 134 { 135 final NameClassPair nameClassPair = elements.next(); 136 final String name = nameClassPair.getName(); 137 final Object object = context.lookup(name); 138 139 // build the key 140 final StringBuilder key = new StringBuilder(); 141 key.append(prefix); 142 if (key.length() > 0) 143 { 144 key.append("."); 145 } 146 key.append(name); 147 148 if (object instanceof Context) 149 { 150 // add the keys of the sub context 151 final Context subcontext = (Context) object; 152 if (!processedCtx.contains(subcontext)) 153 { 154 recursiveGetKeys(keys, subcontext, key.toString(), 155 processedCtx); 156 } 157 } 158 else 159 { 160 // add the key 161 keys.add(key.toString()); 162 } 163 } 164 } 165 finally 166 { 167 // close the enumeration 168 if (elements != null) 169 { 170 elements.close(); 171 } 172 } 173 } 174 175 /** 176 * Returns an iterator with all property keys stored in this configuration. 177 * 178 * @return an iterator with all keys 179 */ 180 @Override 181 protected Iterator<String> getKeysInternal() 182 { 183 return getKeysInternal(""); 184 } 185 186 /** 187 * Returns an iterator with all property keys starting with the given 188 * prefix. 189 * 190 * @param prefix the prefix 191 * @return an iterator with the selected keys 192 */ 193 @Override 194 protected Iterator<String> getKeysInternal(final String prefix) 195 { 196 // build the path 197 final String[] splitPath = StringUtils.split(prefix, "."); 198 199 final List<String> path = Arrays.asList(splitPath); 200 201 try 202 { 203 // find the context matching the specified path 204 final Context context = getContext(path, getBaseContext()); 205 206 // return all the keys under the context found 207 final Set<String> keys = new HashSet<>(); 208 if (context != null) 209 { 210 recursiveGetKeys(keys, context, prefix, new HashSet<Context>()); 211 } 212 else if (containsKey(prefix)) 213 { 214 // add the prefix if it matches exactly a property key 215 keys.add(prefix); 216 } 217 218 return keys.iterator(); 219 } 220 catch (final NameNotFoundException e) 221 { 222 // expected exception, no need to log it 223 return new ArrayList<String>().iterator(); 224 } 225 catch (final NamingException e) 226 { 227 fireError(ConfigurationErrorEvent.READ, 228 ConfigurationErrorEvent.READ, null, null, e); 229 return new ArrayList<String>().iterator(); 230 } 231 } 232 233 /** 234 * Because JNDI is based on a tree configuration, we need to filter down the 235 * tree, till we find the Context specified by the key to start from. 236 * Otherwise return null. 237 * 238 * @param path the path of keys to traverse in order to find the context 239 * @param context the context to start from 240 * @return The context at that key's location in the JNDI tree, or null if not found 241 * @throws NamingException if JNDI has an issue 242 */ 243 private Context getContext(final List<String> path, final Context context) throws NamingException 244 { 245 // return the current context if the path is empty 246 if (path == null || path.isEmpty()) 247 { 248 return context; 249 } 250 251 final String key = path.get(0); 252 253 // search a context matching the key in the context's elements 254 NamingEnumeration<NameClassPair> elements = null; 255 256 try 257 { 258 elements = context.list(""); 259 while (elements.hasMore()) 260 { 261 final NameClassPair nameClassPair = elements.next(); 262 final String name = nameClassPair.getName(); 263 final Object object = context.lookup(name); 264 265 if (object instanceof Context && name.equals(key)) 266 { 267 final Context subcontext = (Context) object; 268 269 // recursive search in the sub context 270 return getContext(path.subList(1, path.size()), subcontext); 271 } 272 } 273 } 274 finally 275 { 276 if (elements != null) 277 { 278 elements.close(); 279 } 280 } 281 282 return null; 283 } 284 285 /** 286 * Returns a flag whether this configuration is empty. 287 * 288 * @return the empty flag 289 */ 290 @Override 291 protected boolean isEmptyInternal() 292 { 293 try 294 { 295 NamingEnumeration<NameClassPair> enumeration = null; 296 297 try 298 { 299 enumeration = getBaseContext().list(""); 300 return !enumeration.hasMore(); 301 } 302 finally 303 { 304 // close the enumeration 305 if (enumeration != null) 306 { 307 enumeration.close(); 308 } 309 } 310 } 311 catch (final NamingException e) 312 { 313 fireError(ConfigurationErrorEvent.READ, 314 ConfigurationErrorEvent.READ, null, null, e); 315 return true; 316 } 317 } 318 319 /** 320 * <p><strong>This operation is not supported and will throw an 321 * UnsupportedOperationException.</strong></p> 322 * 323 * @param key the key 324 * @param value the value 325 * @throws UnsupportedOperationException always thrown as this method is not supported 326 */ 327 @Override 328 protected void setPropertyInternal(final String key, final Object value) 329 { 330 throw new UnsupportedOperationException("This operation is not supported"); 331 } 332 333 /** 334 * Removes the specified property. 335 * 336 * @param key the key of the property to remove 337 */ 338 @Override 339 protected void clearPropertyDirect(final String key) 340 { 341 clearedProperties.add(key); 342 } 343 344 /** 345 * Checks whether the specified key is contained in this configuration. 346 * 347 * @param key the key to check 348 * @return a flag whether this key is stored in this configuration 349 */ 350 @Override 351 protected boolean containsKeyInternal(String key) 352 { 353 if (clearedProperties.contains(key)) 354 { 355 return false; 356 } 357 key = key.replaceAll("\\.", "/"); 358 try 359 { 360 // throws a NamingException if JNDI doesn't contain the key. 361 getBaseContext().lookup(key); 362 return true; 363 } 364 catch (final NameNotFoundException e) 365 { 366 // expected exception, no need to log it 367 return false; 368 } 369 catch (final NamingException e) 370 { 371 fireError(ConfigurationErrorEvent.READ, 372 ConfigurationErrorEvent.READ, key, null, e); 373 return false; 374 } 375 } 376 377 /** 378 * Returns the prefix. 379 * @return the prefix 380 */ 381 public String getPrefix() 382 { 383 return prefix; 384 } 385 386 /** 387 * Sets the prefix. 388 * 389 * @param prefix The prefix to set 390 */ 391 public void setPrefix(final String prefix) 392 { 393 this.prefix = prefix; 394 395 // clear the previous baseContext 396 baseContext = null; 397 } 398 399 /** 400 * Returns the value of the specified property. 401 * 402 * @param key the key of the property 403 * @return the value of this property 404 */ 405 @Override 406 protected Object getPropertyInternal(String key) 407 { 408 if (clearedProperties.contains(key)) 409 { 410 return null; 411 } 412 413 try 414 { 415 key = key.replaceAll("\\.", "/"); 416 return getBaseContext().lookup(key); 417 } 418 catch (final NameNotFoundException e) 419 { 420 // expected exception, no need to log it 421 return null; 422 } 423 catch (final NotContextException nctxex) 424 { 425 // expected exception, no need to log it 426 return null; 427 } 428 catch (final NamingException e) 429 { 430 fireError(ConfigurationErrorEvent.READ, 431 ConfigurationErrorEvent.READ, key, null, e); 432 return null; 433 } 434 } 435 436 /** 437 * <p><strong>This operation is not supported and will throw an 438 * UnsupportedOperationException.</strong></p> 439 * 440 * @param key the key 441 * @param obj the value 442 * @throws UnsupportedOperationException always thrown as this method is not supported 443 */ 444 @Override 445 protected void addPropertyDirect(final String key, final Object obj) 446 { 447 throw new UnsupportedOperationException("This operation is not supported"); 448 } 449 450 /** 451 * Return the base context with the prefix applied. 452 * 453 * @return the base context 454 * @throws NamingException if an error occurs 455 */ 456 public Context getBaseContext() throws NamingException 457 { 458 if (baseContext == null) 459 { 460 baseContext = (Context) getContext().lookup(prefix == null ? "" : prefix); 461 } 462 463 return baseContext; 464 } 465 466 /** 467 * Return the initial context used by this configuration. This context is 468 * independent of the prefix specified. 469 * 470 * @return the initial context 471 */ 472 public Context getContext() 473 { 474 return context; 475 } 476 477 /** 478 * Set the initial context of the configuration. 479 * 480 * @param context the context 481 */ 482 public void setContext(final Context context) 483 { 484 // forget the removed properties 485 clearedProperties.clear(); 486 487 // change the context 488 this.context = context; 489 } 490}