View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.configuration;
19  
20  import java.util.ArrayList;
21  import java.util.Arrays;
22  import java.util.HashSet;
23  import java.util.Iterator;
24  import java.util.List;
25  import java.util.Set;
26  
27  import javax.naming.Context;
28  import javax.naming.InitialContext;
29  import javax.naming.NameClassPair;
30  import javax.naming.NameNotFoundException;
31  import javax.naming.NamingEnumeration;
32  import javax.naming.NamingException;
33  import javax.naming.NotContextException;
34  
35  import org.apache.commons.lang.StringUtils;
36  import org.apache.commons.logging.LogFactory;
37  
38  /**
39   * This Configuration class allows you to interface with a JNDI datasource.
40   * A JNDIConfiguration is read-only, write operations will throw an
41   * UnsupportedOperationException. The clear operations are supported but the
42   * underlying JNDI data source is not changed.
43   *
44   * @author <a href="mailto:epugh@upstate.com">Eric Pugh</a>
45   * @version $Id: JNDIConfiguration.java 1234985 2012-01-23 21:09:09Z oheger $
46   */
47  public class JNDIConfiguration extends AbstractConfiguration
48  {
49      /** The prefix of the context. */
50      private String prefix;
51  
52      /** The initial JNDI context. */
53      private Context context;
54  
55      /** The base JNDI context. */
56      private Context baseContext;
57  
58      /** The Set of keys that have been virtually cleared. */
59      private Set<String> clearedProperties = new HashSet<String>();
60  
61      /**
62       * Creates a JNDIConfiguration using the default initial context as the
63       * root of the properties.
64       *
65       * @throws NamingException thrown if an error occurs when initializing the default context
66       */
67      public JNDIConfiguration() throws NamingException
68      {
69          this((String) null);
70      }
71  
72      /**
73       * Creates a JNDIConfiguration using the default initial context, shifted
74       * with the specified prefix, as the root of the properties.
75       *
76       * @param prefix the prefix
77       *
78       * @throws NamingException thrown if an error occurs when initializing the default context
79       */
80      public JNDIConfiguration(String prefix) throws NamingException
81      {
82          this(new InitialContext(), prefix);
83      }
84  
85      /**
86       * Creates a JNDIConfiguration using the specified initial context as the
87       * root of the properties.
88       *
89       * @param context the initial context
90       */
91      public JNDIConfiguration(Context context)
92      {
93          this(context, null);
94      }
95  
96      /**
97       * Creates a JNDIConfiguration using the specified initial context shifted
98       * by the specified prefix as the root of the properties.
99       *
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 }