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
018 package org.apache.commons.configuration;
019
020 import java.util.ArrayList;
021 import java.util.HashSet;
022 import java.util.Iterator;
023 import java.util.List;
024 import java.util.Set;
025
026 import javax.naming.Context;
027 import javax.naming.InitialContext;
028 import javax.naming.NameClassPair;
029 import javax.naming.NameNotFoundException;
030 import javax.naming.NamingEnumeration;
031 import javax.naming.NamingException;
032 import javax.naming.NotContextException;
033
034 import org.apache.commons.lang.StringUtils;
035 import org.apache.commons.logging.LogFactory;
036
037 /**
038 * This Configuration class allows you to interface with a JNDI datasource.
039 * A JNDIConfiguration is read-only, write operations will throw an
040 * UnsupportedOperationException. The clear operations are supported but the
041 * underlying JNDI data source is not changed.
042 *
043 * @author <a href="mailto:epugh@upstate.com">Eric Pugh</a>
044 * @version $Id: JNDIConfiguration.java 1081926 2011-03-15 20:20:25Z oheger $
045 */
046 public 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 Set 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(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(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(Context context, String prefix)
103 {
104 this.context = context;
105 this.prefix = prefix;
106 setLogger(LogFactory.getLog(getClass()));
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(Set keys, Context context, String prefix, Set processedCtx) throws NamingException
122 {
123 processedCtx.add(context);
124 NamingEnumeration elements = null;
125
126 try
127 {
128 elements = context.list("");
129
130 // iterates through the context's elements
131 while (elements.hasMore())
132 {
133 NameClassPair nameClassPair = (NameClassPair) elements.next();
134 String name = nameClassPair.getName();
135 Object object = context.lookup(name);
136
137 // build the key
138 StringBuffer key = new StringBuffer();
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 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 public Iterator getKeys()
179 {
180 return getKeys("");
181 }
182
183 /**
184 * Returns an iterator with all property keys starting with the given
185 * prefix.
186 *
187 * @param prefix the prefix
188 * @return an iterator with the selected keys
189 */
190 public Iterator getKeys(String prefix)
191 {
192 // build the path
193 String[] splitPath = StringUtils.split(prefix, ".");
194
195 List path = new ArrayList();
196
197 for (int i = 0; i < splitPath.length; i++)
198 {
199 path.add(splitPath[i]);
200 }
201
202 try
203 {
204 // find the context matching the specified path
205 Context context = getContext(path, getBaseContext());
206
207 // return all the keys under the context found
208 Set keys = new HashSet();
209 if (context != null)
210 {
211 recursiveGetKeys(keys, context, prefix, new HashSet());
212 }
213 else if (containsKey(prefix))
214 {
215 // add the prefix if it matches exactly a property key
216 keys.add(prefix);
217 }
218
219 return keys.iterator();
220 }
221 catch (NameNotFoundException e)
222 {
223 // expected exception, no need to log it
224 return new ArrayList().iterator();
225 }
226 catch (NamingException e)
227 {
228 fireError(EVENT_READ_PROPERTY, null, null, e);
229 return new ArrayList().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(List path, 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 String key = (String) path.get(0);
252
253 // search a context matching the key in the context's elements
254 NamingEnumeration elements = null;
255
256 try
257 {
258 elements = context.list("");
259 while (elements.hasMore())
260 {
261 NameClassPair nameClassPair = (NameClassPair) elements.next();
262 String name = nameClassPair.getName();
263 Object object = context.lookup(name);
264
265 if (object instanceof Context && name.equals(key))
266 {
267 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 public boolean isEmpty()
291 {
292 try
293 {
294 NamingEnumeration 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 (NamingException e)
311 {
312 fireError(EVENT_READ_PROPERTY, 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
324 */
325 public void setProperty(String key, Object value)
326 {
327 throw new UnsupportedOperationException("This operation is not supported");
328 }
329
330 /**
331 * Removes the specified property.
332 *
333 * @param key the key of the property to remove
334 */
335 public void clearProperty(String key)
336 {
337 clearedProperties.add(key);
338 }
339
340 /**
341 * Checks whether the specified key is contained in this configuration.
342 *
343 * @param key the key to check
344 * @return a flag whether this key is stored in this configuration
345 */
346 public boolean containsKey(String key)
347 {
348 if (clearedProperties.contains(key))
349 {
350 return false;
351 }
352 key = StringUtils.replace(key, ".", "/");
353 try
354 {
355 // throws a NamingException if JNDI doesn't contain the key.
356 getBaseContext().lookup(key);
357 return true;
358 }
359 catch (NameNotFoundException e)
360 {
361 // expected exception, no need to log it
362 return false;
363 }
364 catch (NamingException e)
365 {
366 fireError(EVENT_READ_PROPERTY, key, null, e);
367 return false;
368 }
369 }
370
371 /**
372 * Returns the prefix.
373 * @return the prefix
374 */
375 public String getPrefix()
376 {
377 return prefix;
378 }
379
380 /**
381 * Sets the prefix.
382 *
383 * @param prefix The prefix to set
384 */
385 public void setPrefix(String prefix)
386 {
387 this.prefix = prefix;
388
389 // clear the previous baseContext
390 baseContext = null;
391 }
392
393 /**
394 * Returns the value of the specified property.
395 *
396 * @param key the key of the property
397 * @return the value of this property
398 */
399 public Object getProperty(String key)
400 {
401 if (clearedProperties.contains(key))
402 {
403 return null;
404 }
405
406 try
407 {
408 key = StringUtils.replace(key, ".", "/");
409 return getBaseContext().lookup(key);
410 }
411 catch (NameNotFoundException e)
412 {
413 // expected exception, no need to log it
414 return null;
415 }
416 catch (NotContextException nctxex)
417 {
418 // expected exception, no need to log it
419 return null;
420 }
421 catch (NamingException e)
422 {
423 fireError(EVENT_READ_PROPERTY, key, null, e);
424 return null;
425 }
426 }
427
428 /**
429 * <p><strong>This operation is not supported and will throw an
430 * UnsupportedOperationException.</strong></p>
431 *
432 * @param key the key
433 * @param obj the value
434 * @throws UnsupportedOperationException
435 */
436 protected void addPropertyDirect(String key, Object obj)
437 {
438 throw new UnsupportedOperationException("This operation is not supported");
439 }
440
441 /**
442 * Return the base context with the prefix applied.
443 *
444 * @return the base context
445 * @throws NamingException if an error occurs
446 */
447 public Context getBaseContext() throws NamingException
448 {
449 if (baseContext == null)
450 {
451 baseContext = (Context) getContext().lookup(prefix == null ? "" : prefix);
452 }
453
454 return baseContext;
455 }
456
457 /**
458 * Return the initial context used by this configuration. This context is
459 * independent of the prefix specified.
460 *
461 * @return the initial context
462 */
463 public Context getContext()
464 {
465 return context;
466 }
467
468 /**
469 * Set the initial context of the configuration.
470 *
471 * @param context the context
472 */
473 public void setContext(Context context)
474 {
475 // forget the removed properties
476 clearedProperties.clear();
477
478 // change the context
479 this.context = context;
480 }
481 }