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