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