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.configuration2;
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.configuration2.event.ConfigurationErrorEvent;
36  import org.apache.commons.configuration2.io.ConfigurationLogger;
37  import org.apache.commons.lang3.StringUtils;
38  
39  /**
40   * This Configuration class allows you to interface with a JNDI datasource. A JNDIConfiguration is read-only, write
41   * operations will throw an UnsupportedOperationException. The clear operations are supported but the underlying JNDI
42   * data source is not changed.
43   */
44  public class JNDIConfiguration extends AbstractConfiguration {
45      /** The prefix of the context. */
46      private String prefix;
47  
48      /** The initial JNDI context. */
49      private Context context;
50  
51      /** The base JNDI context. */
52      private Context baseContext;
53  
54      /** The Set of keys that have been virtually cleared. */
55      private final Set<String> clearedProperties = new HashSet<>();
56  
57      /**
58       * Creates a JNDIConfiguration using the default initial context as the root of the properties.
59       *
60       * @throws NamingException thrown if an error occurs when initializing the default context
61       */
62      public JNDIConfiguration() throws NamingException {
63          this((String) null);
64      }
65  
66      /**
67       * Creates a JNDIConfiguration using the specified initial context as the root of the properties.
68       *
69       * @param context the initial context
70       */
71      public JNDIConfiguration(final Context context) {
72          this(context, null);
73      }
74  
75      /**
76       * Creates a JNDIConfiguration using the specified initial context shifted by the specified prefix as the root of the
77       * properties.
78       *
79       * @param context the initial context
80       * @param prefix the prefix
81       */
82      public JNDIConfiguration(final Context context, final String prefix) {
83          this.context = context;
84          this.prefix = prefix;
85          initLogger(new ConfigurationLogger(JNDIConfiguration.class));
86          addErrorLogListener();
87      }
88  
89      /**
90       * Creates a JNDIConfiguration using the default initial context, shifted with the specified prefix, as the root of the
91       * properties.
92       *
93       * @param prefix the prefix
94       *
95       * @throws NamingException thrown if an error occurs when initializing the default context
96       */
97      public JNDIConfiguration(final String prefix) throws NamingException {
98          this(new InitialContext(), prefix);
99      }
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      * @since 2.11.0
154      */
155     @Override
156     protected boolean containsValueInternal(final Object value) {
157         return contains(getKeys(), value);
158     }
159 
160     /**
161      * Gets the base context with the prefix applied.
162      *
163      * @return the base context
164      * @throws NamingException if an error occurs
165      */
166     public Context getBaseContext() throws NamingException {
167         if (baseContext == null) {
168             baseContext = (Context) getContext().lookup(prefix == null ? "" : prefix);
169         }
170 
171         return baseContext;
172     }
173 
174     /**
175      * Gets the initial context used by this configuration. This context is independent of the prefix specified.
176      *
177      * @return the initial context
178      */
179     public Context getContext() {
180         return context;
181     }
182 
183     /**
184      * Because JNDI is based on a tree configuration, we need to filter down the tree, till we find the Context specified by
185      * the key to start from. Otherwise return null.
186      *
187      * @param path the path of keys to traverse in order to find the context
188      * @param context the context to start from
189      * @return The context at that key's location in the JNDI tree, or null if not found
190      * @throws NamingException if JNDI has an issue
191      */
192     private Context getContext(final List<String> path, final Context context) throws NamingException {
193         // return the current context if the path is empty
194         if (path == null || path.isEmpty()) {
195             return context;
196         }
197 
198         final String key = path.get(0);
199 
200         // search a context matching the key in the context's elements
201         NamingEnumeration<NameClassPair> elements = null;
202 
203         try {
204             elements = context.list("");
205             while (elements.hasMore()) {
206                 final NameClassPair nameClassPair = elements.next();
207                 final String name = nameClassPair.getName();
208                 final Object object = context.lookup(name);
209 
210                 if (object instanceof Context && name.equals(key)) {
211                     final Context subcontext = (Context) object;
212 
213                     // recursive search in the sub context
214                     return getContext(path.subList(1, path.size()), subcontext);
215                 }
216             }
217         } finally {
218             if (elements != null) {
219                 elements.close();
220             }
221         }
222 
223         return null;
224     }
225 
226     /**
227      * Gets an iterator with all property keys stored in this configuration.
228      *
229      * @return an iterator with all keys
230      */
231     @Override
232     protected Iterator<String> getKeysInternal() {
233         return getKeysInternal("");
234     }
235 
236     /**
237      * Gets an iterator with all property keys starting with the given prefix.
238      *
239      * @param prefix the prefix
240      * @return an iterator with the selected keys
241      */
242     @Override
243     protected Iterator<String> getKeysInternal(final String prefix) {
244         // build the path
245         final String[] splitPath = StringUtils.split(prefix, ".");
246 
247         final List<String> path = Arrays.asList(splitPath);
248 
249         try {
250             // find the context matching the specified path
251             final Context context = getContext(path, getBaseContext());
252 
253             // return all the keys under the context found
254             final Set<String> keys = new HashSet<>();
255             if (context != null) {
256                 recursiveGetKeys(keys, context, prefix, new HashSet<>());
257             } else if (containsKey(prefix)) {
258                 // add the prefix if it matches exactly a property key
259                 keys.add(prefix);
260             }
261 
262             return keys.iterator();
263         } catch (final NameNotFoundException e) {
264             // expected exception, no need to log it
265             return new ArrayList<String>().iterator();
266         } catch (final NamingException e) {
267             fireError(ConfigurationErrorEvent.READ, ConfigurationErrorEvent.READ, null, null, e);
268             return new ArrayList<String>().iterator();
269         }
270     }
271 
272     /**
273      * Gets the prefix.
274      *
275      * @return the prefix
276      */
277     public String getPrefix() {
278         return prefix;
279     }
280 
281     /**
282      * Gets the value of the specified property.
283      *
284      * @param key the key of the property
285      * @return the value of this property
286      */
287     @Override
288     protected Object getPropertyInternal(String key) {
289         if (clearedProperties.contains(key)) {
290             return null;
291         }
292 
293         try {
294             key = key.replace('.', '/');
295             return getBaseContext().lookup(key);
296         } catch (final NameNotFoundException | NotContextException nctxex) {
297             // expected exception, no need to log it
298             return null;
299         } catch (final NamingException e) {
300             fireError(ConfigurationErrorEvent.READ, ConfigurationErrorEvent.READ, key, null, e);
301             return null;
302         }
303     }
304 
305     /**
306      * Returns a flag whether this configuration is empty.
307      *
308      * @return the empty flag
309      */
310     @Override
311     protected boolean isEmptyInternal() {
312         try {
313             NamingEnumeration<NameClassPair> enumeration = null;
314 
315             try {
316                 enumeration = getBaseContext().list("");
317                 return !enumeration.hasMore();
318             } finally {
319                 // close the enumeration
320                 if (enumeration != null) {
321                     enumeration.close();
322                 }
323             }
324         } catch (final NamingException e) {
325             fireError(ConfigurationErrorEvent.READ, ConfigurationErrorEvent.READ, null, null, e);
326             return true;
327         }
328     }
329 
330     /**
331      * This method recursive traverse the JNDI tree, looking for Context objects. When it finds them, it traverses them as
332      * well. Otherwise it just adds the values to the list of keys found.
333      *
334      * @param keys All the keys that have been found.
335      * @param context The parent context
336      * @param prefix What prefix we are building on.
337      * @param processedCtx a set with the so far processed objects
338      * @throws NamingException If JNDI has an issue.
339      */
340     private void recursiveGetKeys(final Set<String> keys, final Context context, final String prefix, final Set<Context> processedCtx) throws NamingException {
341         processedCtx.add(context);
342         NamingEnumeration<NameClassPair> elements = null;
343 
344         try {
345             elements = context.list("");
346 
347             // iterates through the context's elements
348             while (elements.hasMore()) {
349                 final NameClassPair nameClassPair = elements.next();
350                 final String name = nameClassPair.getName();
351                 final Object object = context.lookup(name);
352 
353                 // build the key
354                 final StringBuilder keyBuilder = new StringBuilder();
355                 keyBuilder.append(prefix);
356                 if (keyBuilder.length() > 0) {
357                     keyBuilder.append(".");
358                 }
359                 keyBuilder.append(name);
360 
361                 if (object instanceof Context) {
362                     // add the keys of the sub context
363                     final Context subcontext = (Context) object;
364                     if (!processedCtx.contains(subcontext)) {
365                         recursiveGetKeys(keys, subcontext, keyBuilder.toString(), processedCtx);
366                     }
367                 } else {
368                     // add the key
369                     keys.add(keyBuilder.toString());
370                 }
371             }
372         } finally {
373             // close the enumeration
374             if (elements != null) {
375                 elements.close();
376             }
377         }
378     }
379 
380     /**
381      * Sets the initial context of the configuration.
382      *
383      * @param context the context
384      */
385     public void setContext(final Context context) {
386         // forget the removed properties
387         clearedProperties.clear();
388 
389         // change the context
390         this.context = context;
391     }
392 
393     /**
394      * Sets the prefix.
395      *
396      * @param prefix The prefix to set
397      */
398     public void setPrefix(final String prefix) {
399         this.prefix = prefix;
400 
401         // clear the previous baseContext
402         baseContext = null;
403     }
404 
405     /**
406      * <p>
407      * <strong>This operation is not supported and will throw an UnsupportedOperationException.</strong>
408      * </p>
409      *
410      * @param key the key
411      * @param value the value
412      * @throws UnsupportedOperationException always thrown as this method is not supported
413      */
414     @Override
415     protected void setPropertyInternal(final String key, final Object value) {
416         throw new UnsupportedOperationException("This operation is not supported");
417     }
418 }