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