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