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