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.Arrays;
022 import java.util.HashSet;
023 import java.util.Iterator;
024 import java.util.List;
025 import java.util.Set;
026
027 import javax.naming.Context;
028 import javax.naming.InitialContext;
029 import javax.naming.NameClassPair;
030 import javax.naming.NameNotFoundException;
031 import javax.naming.NamingEnumeration;
032 import javax.naming.NamingException;
033 import javax.naming.NotContextException;
034
035 import org.apache.commons.lang.StringUtils;
036 import org.apache.commons.logging.LogFactory;
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 1234985 2012-01-23 21:09:09Z oheger $
046 */
047 public 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 Set<String> clearedProperties = new HashSet<String>();
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(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(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(Context context, String prefix)
104 {
105 this.context = context;
106 this.prefix = prefix;
107 setLogger(LogFactory.getLog(getClass()));
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(Set<String> keys, Context context, String prefix,
123 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 NameClassPair nameClassPair = elements.next();
136 String name = nameClassPair.getName();
137 Object object = context.lookup(name);
138
139 // build the key
140 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 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 public Iterator<String> getKeys()
181 {
182 return getKeys("");
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 public Iterator<String> getKeys(String prefix)
194 {
195 // build the path
196 String[] splitPath = StringUtils.split(prefix, ".");
197
198 List<String> path = Arrays.asList(splitPath);
199
200 try
201 {
202 // find the context matching the specified path
203 Context context = getContext(path, getBaseContext());
204
205 // return all the keys under the context found
206 Set<String> keys = new HashSet<String>();
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 (NameNotFoundException e)
220 {
221 // expected exception, no need to log it
222 return new ArrayList<String>().iterator();
223 }
224 catch (NamingException e)
225 {
226 fireError(EVENT_READ_PROPERTY, 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(List<String> path, 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 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 NameClassPair nameClassPair = elements.next();
260 String name = nameClassPair.getName();
261 Object object = context.lookup(name);
262
263 if (object instanceof Context && name.equals(key))
264 {
265 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 public boolean isEmpty()
289 {
290 try
291 {
292 NamingEnumeration<NameClassPair> enumeration = null;
293
294 try
295 {
296 enumeration = getBaseContext().list("");
297 return !enumeration.hasMore();
298 }
299 finally
300 {
301 // close the enumeration
302 if (enumeration != null)
303 {
304 enumeration.close();
305 }
306 }
307 }
308 catch (NamingException e)
309 {
310 fireError(EVENT_READ_PROPERTY, null, null, e);
311 return true;
312 }
313 }
314
315 /**
316 * <p><strong>This operation is not supported and will throw an
317 * UnsupportedOperationException.</strong></p>
318 *
319 * @param key the key
320 * @param value the value
321 * @throws UnsupportedOperationException
322 */
323 @Override
324 public void setProperty(String key, Object value)
325 {
326 throw new UnsupportedOperationException("This operation is not supported");
327 }
328
329 /**
330 * Removes the specified property.
331 *
332 * @param key the key of the property to remove
333 */
334 @Override
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 = key.replaceAll("\\.", "/");
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 = key.replaceAll("\\.", "/");
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 @Override
437 protected void addPropertyDirect(String key, Object obj)
438 {
439 throw new UnsupportedOperationException("This operation is not supported");
440 }
441
442 /**
443 * Return the base context with the prefix applied.
444 *
445 * @return the base context
446 * @throws NamingException if an error occurs
447 */
448 public Context getBaseContext() throws NamingException
449 {
450 if (baseContext == null)
451 {
452 baseContext = (Context) getContext().lookup(prefix == null ? "" : prefix);
453 }
454
455 return baseContext;
456 }
457
458 /**
459 * Return the initial context used by this configuration. This context is
460 * independent of the prefix specified.
461 *
462 * @return the initial context
463 */
464 public Context getContext()
465 {
466 return context;
467 }
468
469 /**
470 * Set the initial context of the configuration.
471 *
472 * @param context the context
473 */
474 public void setContext(Context context)
475 {
476 // forget the removed properties
477 clearedProperties.clear();
478
479 // change the context
480 this.context = context;
481 }
482 }